-
Notifications
You must be signed in to change notification settings - Fork 1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(ui): implement jobs management (#1463)
* feat(ui): job runs table * update: jobs table * feat: jobrun detail page * fix: detail link * update * feat: update api params * [autofix.ci] apply automated fixes * update rich text style * [autofix.ci] apply automated fixes * update * update(ui): layout * [autofix.ci] apply automated fixes * Update ee/tabby-ui/app/(dashboard)/components/sidebar.tsx Co-authored-by: Meng Zhang <[email protected]> * Update ee/tabby-ui/app/(dashboard)/(logs)/jobs/detail/page.tsx Co-authored-by: Meng Zhang <[email protected]> * Update ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/jobs.tsx Co-authored-by: Meng Zhang <[email protected]> * fix sidebar * [autofix.ci] apply automated fixes * update: log - 30height * format * fix log display * update * [autofix.ci] apply automated fixes * update * [autofix.ci] apply automated fixes * Update ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/jobs-table.tsx Co-authored-by: Meng Zhang <[email protected]> * Update ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/jobs-table.tsx Co-authored-by: Meng Zhang <[email protected]> * update * [autofix.ci] apply automated fixes * exitcode * update * format * update --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Meng Zhang <[email protected]>
- Loading branch information
1 parent
3fb6dbb
commit a3fcc65
Showing
17 changed files
with
702 additions
and
50 deletions.
There are no files selected for viewing
99 changes: 99 additions & 0 deletions
99
ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/job-detail.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
'use client' | ||
|
||
import React from 'react' | ||
import { useSearchParams } from 'next/navigation' | ||
import Ansi from '@curvenote/ansi-to-react' | ||
import { useQuery } from 'urql' | ||
|
||
import { listJobRuns } from '@/lib/tabby/query' | ||
import { cn } from '@/lib/utils' | ||
import { IconAlertTriangle, IconTerminalSquare } from '@/components/ui/icons' | ||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' | ||
import { ListSkeleton } from '@/components/skeleton' | ||
|
||
import { JobsTable } from './jobs-table' | ||
|
||
export default function JobRunDetail() { | ||
const searchParams = useSearchParams() | ||
const id = searchParams.get('id') | ||
const [{ data, error, fetching }, reexecuteQuery] = useQuery({ | ||
query: listJobRuns, | ||
variables: { ids: [id as string] }, | ||
pause: !id | ||
}) | ||
|
||
const edges = data?.jobRuns?.edges?.slice(0, 1) | ||
const currentNode = data?.jobRuns?.edges?.[0]?.node | ||
|
||
React.useEffect(() => { | ||
let timer: number | ||
if (currentNode?.createdAt && !currentNode?.finishedAt) { | ||
timer = window.setTimeout(() => { | ||
reexecuteQuery() | ||
}, 5000) | ||
} | ||
|
||
return () => { | ||
if (timer) { | ||
clearInterval(timer) | ||
} | ||
} | ||
}, [currentNode]) | ||
|
||
return ( | ||
<> | ||
{fetching ? ( | ||
<ListSkeleton /> | ||
) : ( | ||
<div className="flex flex-1 flex-col items-stretch gap-2"> | ||
<JobsTable jobs={edges?.slice(0, 1)} shouldRedirect={false} /> | ||
<Tabs defaultValue="stdout" className="flex flex-1 flex-col"> | ||
<TabsList className="grid w-[400px] grid-cols-2"> | ||
<TabsTrigger value="stdout"> | ||
<IconTerminalSquare className="mr-1" /> | ||
stdout | ||
</TabsTrigger> | ||
<TabsTrigger value="stderr"> | ||
<IconAlertTriangle className="mr-1" /> | ||
stderr | ||
</TabsTrigger> | ||
</TabsList> | ||
<div className="flex flex-1 flex-col"> | ||
<TabsContent value="stdout"> | ||
<StdoutView value={currentNode?.stdout} /> | ||
</TabsContent> | ||
<TabsContent value="stderr"> | ||
<StdoutView value={currentNode?.stderr} /> | ||
</TabsContent> | ||
</div> | ||
</Tabs> | ||
</div> | ||
)} | ||
</> | ||
) | ||
} | ||
|
||
function StdoutView({ | ||
children, | ||
className, | ||
value, | ||
...rest | ||
}: React.HTMLAttributes<HTMLDivElement> & { value?: string }) { | ||
return ( | ||
<div | ||
className={cn( | ||
'mt-2 h-[66vh] w-full overflow-y-auto overflow-x-hidden rounded-lg border bg-gray-50 font-mono text-[0.9rem] dark:bg-gray-800', | ||
className | ||
)} | ||
{...rest} | ||
> | ||
{value ? ( | ||
<pre className="whitespace-pre-wrap p-4"> | ||
<Ansi>{value}</Ansi> | ||
</pre> | ||
) : ( | ||
<div className="p-4">No Data</div> | ||
)} | ||
</div> | ||
) | ||
} |
61 changes: 61 additions & 0 deletions
61
ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/job-list.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
'use client' | ||
|
||
import React from 'react' | ||
import { useQuery } from 'urql' | ||
|
||
import { DEFAULT_PAGE_SIZE } from '@/lib/constants' | ||
import { ListJobRunsQueryVariables } from '@/lib/gql/generates/graphql' | ||
import { useIsQueryInitialized } from '@/lib/tabby/gql' | ||
import { listJobRuns } from '@/lib/tabby/query' | ||
import { Button } from '@/components/ui/button' | ||
import { IconSpinner } from '@/components/ui/icons' | ||
import { ListSkeleton } from '@/components/skeleton' | ||
|
||
import { JobsTable } from './jobs-table' | ||
|
||
const PAGE_SIZE = DEFAULT_PAGE_SIZE | ||
export function JobRuns() { | ||
const [variables, setVariables] = React.useState<ListJobRunsQueryVariables>({ | ||
last: PAGE_SIZE | ||
}) | ||
const [{ data, error, fetching, stale }] = useQuery({ | ||
query: listJobRuns, | ||
variables | ||
}) | ||
|
||
const [initialized] = useIsQueryInitialized({ data, error, stale }) | ||
|
||
const edges = data?.jobRuns?.edges | ||
const pageInfo = data?.jobRuns?.pageInfo | ||
const hasNextPage = pageInfo?.hasPreviousPage | ||
|
||
const fetchNextPage = () => { | ||
setVariables({ last: PAGE_SIZE, before: pageInfo?.startCursor }) | ||
} | ||
|
||
const displayJobs = React.useMemo(() => { | ||
return edges?.slice().reverse() | ||
}, [edges]) | ||
|
||
return ( | ||
<> | ||
{initialized ? ( | ||
<> | ||
<JobsTable jobs={displayJobs} /> | ||
{hasNextPage && ( | ||
<div className="mt-8 text-center"> | ||
<Button disabled={fetching} onClick={fetchNextPage}> | ||
{fetching && ( | ||
<IconSpinner className="mr-2 h-4 w-4 animate-spin" /> | ||
)} | ||
load more | ||
</Button> | ||
</div> | ||
)} | ||
</> | ||
) : ( | ||
<ListSkeleton /> | ||
)} | ||
</> | ||
) | ||
} |
148 changes: 148 additions & 0 deletions
148
ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/jobs-table.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
import React from 'react' | ||
import { useRouter } from 'next/navigation' | ||
import { isNil } from 'lodash-es' | ||
import moment from 'moment' | ||
|
||
import { ListJobRunsQuery } from '@/lib/gql/generates/graphql' | ||
import { cn } from '@/lib/utils' | ||
import { Badge } from '@/components/ui/badge' | ||
import { | ||
IconCheckCircled, | ||
IconCrossCircled, | ||
IconInfoCircled | ||
} from '@/components/ui/icons' | ||
import { | ||
Table, | ||
TableBody, | ||
TableCell, | ||
TableHead, | ||
TableHeader, | ||
TableRow | ||
} from '@/components/ui/table' | ||
|
||
type TJobRun = ListJobRunsQuery['jobRuns']['edges'][0] | ||
interface JobsTableProps { | ||
jobs: TJobRun[] | undefined | ||
shouldRedirect?: boolean | ||
} | ||
|
||
export const JobsTable: React.FC<JobsTableProps> = ({ | ||
jobs, | ||
shouldRedirect = true | ||
}) => { | ||
const router = useRouter() | ||
return ( | ||
<Table> | ||
<TableHeader> | ||
<TableRow> | ||
<TableHead className="w-[200px]">Start Time</TableHead> | ||
<TableHead className="w-[100px]">Duration</TableHead> | ||
<TableHead className="w-[100px]">Job</TableHead> | ||
<TableHead className="w-[100px] text-center">Status</TableHead> | ||
</TableRow> | ||
</TableHeader> | ||
<TableBody> | ||
{!jobs?.length ? ( | ||
<TableRow> | ||
<TableCell | ||
colSpan={shouldRedirect ? 4 : 3} | ||
className="h-[100px] text-center" | ||
> | ||
No Data | ||
</TableCell> | ||
</TableRow> | ||
) : ( | ||
<> | ||
{jobs?.map(x => { | ||
return ( | ||
<TableRow | ||
key={x.node.id} | ||
className={cn(shouldRedirect && 'cursor-pointer')} | ||
onClick={e => { | ||
if (shouldRedirect) { | ||
router.push(`/jobs/detail?id=${x.node.id}`) | ||
} | ||
}} | ||
> | ||
<TableCell> | ||
{moment(x.node.createdAt).format('MMMM D, YYYY h:mm a')} | ||
</TableCell> | ||
<TableCell> | ||
{isNil(x.node?.exitCode) | ||
? 'Running' | ||
: getJobDuration(x.node)} | ||
</TableCell> | ||
<TableCell> | ||
<Badge variant="secondary">{x.node.job}</Badge> | ||
</TableCell> | ||
<TableCell> | ||
<div className="flex items-center justify-center"> | ||
<JobStatusIcon node={x} /> | ||
</div> | ||
</TableCell> | ||
</TableRow> | ||
) | ||
})} | ||
</> | ||
)} | ||
</TableBody> | ||
</Table> | ||
) | ||
} | ||
|
||
function getJobDuration({ | ||
createdAt, | ||
finishedAt | ||
}: { | ||
createdAt: string | ||
finishedAt?: string | ||
}) { | ||
if (!createdAt || !finishedAt) return undefined | ||
|
||
let duration = moment.duration(moment(finishedAt).diff(createdAt)) | ||
return formatDuration(duration) | ||
} | ||
|
||
function formatDuration(duration: moment.Duration) { | ||
const hours = duration.hours() | ||
const minutes = duration.minutes() | ||
const seconds = duration.seconds() | ||
|
||
let formattedDuration = '' | ||
|
||
if (hours > 0) { | ||
formattedDuration += `${hours}h` | ||
} | ||
|
||
if (minutes > 0) { | ||
if (formattedDuration.length > 0) { | ||
formattedDuration += ' ' | ||
} | ||
|
||
formattedDuration += `${minutes}min` | ||
} | ||
|
||
if (seconds > 0) { | ||
if (formattedDuration.length > 0) { | ||
formattedDuration += ' ' | ||
} | ||
|
||
formattedDuration += `${seconds}s` | ||
} | ||
|
||
return formattedDuration | ||
} | ||
|
||
function JobStatusIcon({ node }: { node: TJobRun }) { | ||
if (!node) return null | ||
const exitCode = node?.node?.exitCode | ||
|
||
if (isNil(exitCode)) { | ||
return <IconInfoCircled /> | ||
} | ||
if (exitCode === 0) { | ||
return <IconCheckCircled /> | ||
} | ||
|
||
return <IconCrossCircled /> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { JobRuns } from './job-list' | ||
|
||
export default function JobRunsPage() { | ||
return ( | ||
<> | ||
<JobRuns /> | ||
</> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import JobRunDetail from '../components/job-detail' | ||
|
||
export default function JobDetailPage() { | ||
return ( | ||
<> | ||
<JobRunDetail /> | ||
</> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { Metadata } from 'next' | ||
|
||
import JobRunsPage from './components/jobs' | ||
|
||
export const metadata: Metadata = { | ||
title: 'Job runs' | ||
} | ||
|
||
export default function IndexPage() { | ||
return <JobRunsPage /> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export default function LogsLayout({ | ||
children | ||
}: { | ||
children: React.ReactNode | ||
}) { | ||
return <div className="flex flex-col p-6">{children}</div> | ||
} |
Oops, something went wrong.