Skip to content

Commit

Permalink
feat(ui): implement jobs management (#1463)
Browse files Browse the repository at this point in the history
* 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
3 people authored Feb 18, 2024
1 parent 3fb6dbb commit a3fcc65
Show file tree
Hide file tree
Showing 17 changed files with 702 additions and 50 deletions.
99 changes: 99 additions & 0 deletions ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/job-detail.tsx
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 ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/job-list.tsx
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 ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/jobs-table.tsx
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 />
}
9 changes: 9 additions & 0 deletions ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/jobs.tsx
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 />
</>
)
}
9 changes: 9 additions & 0 deletions ee/tabby-ui/app/(dashboard)/(logs)/jobs/detail/page.tsx
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 />
</>
)
}
11 changes: 11 additions & 0 deletions ee/tabby-ui/app/(dashboard)/(logs)/jobs/page.tsx
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 />
}
7 changes: 7 additions & 0 deletions ee/tabby-ui/app/(dashboard)/(logs)/layout.tsx
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>
}
Loading

0 comments on commit a3fcc65

Please sign in to comment.