Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(notification): send notification to admin when job failed #3653

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ export default function JobRunDetail() {
(stateLabel === 'Pending' || stateLabel === 'Running') &&
!currentNode?.stdout

const handleBackNavigation = () => {
if (typeof window !== 'undefined' && window.history.length <= 1) {
router.push('/jobs')
} else {
router.back()
}
}

React.useEffect(() => {
let timer: number
if (currentNode?.createdAt && !currentNode?.finishedAt) {
Expand All @@ -61,7 +69,7 @@ export default function JobRunDetail() {
{currentNode && (
<>
<div
onClick={() => router.back()}
onClick={handleBackNavigation}
className="-ml-1 flex cursor-pointer items-center transition-opacity hover:opacity-60"
>
<IconChevronLeft className="mr-1 h-6 w-6" />
Expand Down
4 changes: 4 additions & 0 deletions ee/tabby-ui/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,7 @@
.dialog-without-close-btn > button {
display: none;
}

.unread-notification::before {
@apply content-[''] float-left w-2 h-2 mr-1.5 mt-2 rounded-full bg-red-400;
}
39 changes: 17 additions & 22 deletions ee/tabby-ui/components/notification-box.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@ import { useMutation } from '@/lib/tabby/gql'
import { notificationsQuery } from '@/lib/tabby/query'
import { ArrayElementType } from '@/lib/types'
import { cn } from '@/lib/utils'

import LoadingWrapper from './loading-wrapper'
import { ListSkeleton } from './skeleton'
import { Button } from './ui/button'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger
} from './ui/dropdown-menu'
import { IconBell, IconCheck } from './ui/icons'
import { Separator } from './ui/separator'
import { Tabs, TabsList, TabsTrigger } from './ui/tabs'
} from '@/components/ui/dropdown-menu'
import { IconBell, IconCheck } from '@/components/ui/icons'
import { Separator } from '@/components/ui/separator'
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
import LoadingWrapper from '@/components/loading-wrapper'
import { MemoizedReactMarkdown } from '@/components/markdown'
import { ListSkeleton } from '@/components/skeleton'

interface Props extends HTMLAttributes<HTMLDivElement> {}

Expand Down Expand Up @@ -89,7 +89,7 @@ export function NotificationBox({ className, ...rest }: Props) {
</div>
<Separator />
<Tabs
className="relative my-2 flex-1 overflow-y-auto px-4"
className="relative my-2 flex-1 overflow-y-auto px-5"
defaultValue="unread"
>
<TabsList className="sticky top-0 z-10 grid w-full grid-cols-2">
Expand Down Expand Up @@ -156,8 +156,6 @@ interface NotificationItemProps extends HTMLAttributes<HTMLDivElement> {
}

function NotificationItem({ data }: NotificationItemProps) {
const { title, content } = resolveNotification(data.content)

const markNotificationsRead = useMutation(markNotificationsReadMutation)

const onClickMarkRead = () => {
Expand All @@ -168,17 +166,14 @@ function NotificationItem({ data }: NotificationItemProps) {

return (
<div className="space-y-1.5">
<div className="space-y-1.5">
<div className="flex items-center gap-1.5 overflow-hidden text-sm font-medium">
{!data.read && (
<span className="h-2 w-2 shrink-0 rounded-full bg-red-400"></span>
)}
<span className="flex-1 truncate">{title}</span>
</div>
<div className="whitespace-pre-wrap break-words text-sm text-muted-foreground">
{content}
</div>
</div>
<MemoizedReactMarkdown
className={cn(
'prose max-w-none break-words text-sm dark:prose-invert prose-p:my-1 prose-p:leading-relaxed',
{ 'unread-notification': !data.read }
)}
>
{data.content}
</MemoizedReactMarkdown>
<div className="flex items-center justify-between text-xs text-muted-foreground">
<span className="text-muted-foreground">
{formatNotificationTime(data.createdAt)}
Expand Down
32 changes: 17 additions & 15 deletions ee/tabby-webserver/src/service/background_job/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,19 @@ impl DbMaintainanceJob {
context: Arc<dyn ContextService>,
db: DbConn,
) -> tabby_schema::Result<()> {
let mut errors = vec![];
let mut exit_code = 0;
zwpaper marked this conversation as resolved.
Show resolved Hide resolved

if let Err(e) = db.delete_expired_token().await {
errors.push(format!("Failed to delete expired token: {}", e));
exit_code = -1;
logkit::warn!(exit_code = exit_code; "Failed to delete expired tokens: {}", e);
zwpaper marked this conversation as resolved.
Show resolved Hide resolved
};
if let Err(e) = db.delete_expired_password_resets().await {
errors.push(format!("Failed to delete expired password resets: {}", e));
exit_code = -1;
logkit::warn!(exit_code = exit_code; "Failed to delete expired password resets: {}", e);
};
if let Err(e) = db.delete_expired_ephemeral_threads().await {
errors.push(format!("Failed to delete expired ephemeral threads: {}", e));
exit_code = -1;
logkit::warn!(exit_code = exit_code; "Failed to delete expired ephemeral threads: {}", e);
};

// Read all active sources
Expand All @@ -44,27 +47,26 @@ impl DbMaintainanceJob {
.delete_unused_source_id_read_access_policy(&active_source_ids)
.await
{
errors.push(format!(
"Failed to delete unused source id read access policy: {}",
e
));
exit_code = -1;
logkit::warn!(exit_code = exit_code; "Failed to delete unused source id read access policy: {}", e);
};
}
Err(e) => {
errors.push(format!("Failed to read active sources: {}", e));
exit_code = -1;
logkit::warn!(exit_code = exit_code; "Failed to read active sources: {}", e);
}
}

if let Err(e) = Self::data_retention(now, &db).await {
errors.push(format!("Failed to run data retention job: {}", e));
exit_code = -1;
logkit::warn!(exit_code = exit_code; "Failed to run data retention job: {}", e);
}

if errors.is_empty() {
if exit_code == 0 {
Ok(())
} else {
Err(CoreError::Other(anyhow::anyhow!(
"Failed to run db maintenance job:\n{}",
errors.join(";\n")
"Failed to run db maintenance job"
)))
}
}
Expand All @@ -90,8 +92,8 @@ impl DbMaintainanceJob {
Ok(())
} else {
Err(CoreError::Other(anyhow::anyhow!(
"Failed to run data retention job:\n{}",
errors.join(";\n")
"{}",
errors.join(";\n\n")
)))
}
}
Expand Down
12 changes: 8 additions & 4 deletions ee/tabby-webserver/src/service/background_job/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,13 @@ impl SchedulerGitJob {
git_repository: Arc<dyn GitRepositoryService>,
job: Arc<dyn JobService>,
) -> tabby_schema::Result<()> {
let repositories = git_repository
.repository_list()
.await
.context("Must be able to retrieve repositories for sync")?;
let repositories = match git_repository.repository_list().await {
Ok(repos) => repos,
Err(err) => {
logkit::warn!(exit_code = -1; "Failed to list repositories: {}", err);
zwpaper marked this conversation as resolved.
Show resolved Hide resolved
return Err(err);
}
};

let repositories: Vec<_> = repositories
.into_iter()
Expand All @@ -57,6 +60,7 @@ impl SchedulerGitJob {
.trigger(BackgroundJobEvent::SchedulerGitRepository(repository).to_command())
.await;
}

Ok(())
}
}
9 changes: 8 additions & 1 deletion ee/tabby-webserver/src/service/background_job/helper/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ mod logger;
pub use cron::CronStream;
pub use logger::JobLogger;

pub trait Job {
pub trait Job: serde::Serialize {
const NAME: &'static str;

fn name(&self) -> &'static str {
Self::NAME
}
fn to_command(&self) -> String {
serde_json::to_string(self).unwrap()
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use std::sync::Arc;

use serde::Serialize;
use tabby_index::public::{run_index_garbage_collection, CodeIndexer};
use tabby_schema::{context::ContextService, repository::RepositoryService};

use super::helper::Job;

#[derive(Serialize)]
pub struct IndexGarbageCollection;

impl Job for IndexGarbageCollection {
Expand All @@ -18,17 +20,32 @@ impl IndexGarbageCollection {
context: Arc<dyn ContextService>,
) -> tabby_schema::Result<()> {
// Run garbage collection on the index
let sources = context
.read(None)
.await?
let sources = match context.read(None).await {
Ok(sources) => sources,
Err(err) => {
logkit::warn!(exit_code = -1; "Failed to list sources: {}", err);
zwpaper marked this conversation as resolved.
Show resolved Hide resolved
return Err(err);
}
};
let sources = sources
.sources
.into_iter()
.map(|x| x.source_id())
.collect::<Vec<_>>();
run_index_garbage_collection(sources)?;

if let Err(e) = run_index_garbage_collection(sources) {
logkit::warn!(exit_code = -1; "Failed to run index garbage collection: {}", e);
return Err(e.into());
}

// Run garbage collection on the code repositories (cloned directories)
let repositories = repository.list_all_code_repository().await?;
let repositories = match repository.list_all_code_repository().await {
Ok(repos) => repos,
Err(err) => {
logkit::warn!(exit_code = -1; "Failed to list repositories: {}", err);
return Err(err);
}
};
let mut code = CodeIndexer::default();
code.garbage_collection(&repositories).await;

Expand Down
18 changes: 14 additions & 4 deletions ee/tabby-webserver/src/service/background_job/license_check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ use std::sync::Arc;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use tabby_schema::{
context::ContextService,
license::{LicenseService, LicenseType},
notification::{NotificationRecipient, NotificationService},
};
Expand All @@ -23,20 +22,31 @@ impl LicenseCheckJob {
license_service: Arc<dyn LicenseService>,
notification_service: Arc<dyn NotificationService>,
) -> tabby_schema::Result<()> {
let license = license_service.read().await?;
let license = match license_service.read().await {
Ok(license) => license,
Err(err) => {
logkit::warn!(exit_code = -1; "Failed to read license: {}", err);
return Err(err);
}
};
if license.r#type == LicenseType::Community {
return Ok(());
}
if let Some(expire_in_days) = license.expire_in_days() {
if expire_in_days < 7 && expire_in_days > 0 {
notification_service
if let Err(e) = notification_service
.create(
NotificationRecipient::Admin,
&make_expring_message(expire_in_days),
)
.await?;
.await
{
logkit::warn!(exit_code = -1; "Failed to create notification: {}", e);
return Err(e);
}
}
}

Ok(())
}
}
Expand Down
Loading
Loading