Skip to content

Commit

Permalink
feat(ui): new layout for the Jobs page (#1667)
Browse files Browse the repository at this point in the history
* feat(ui): new job layout

* clean

* color in job detail

* [autofix.ci] apply automated fixes

* fdetail fix

* [autofix.ci] apply automated fixes

* ui update

* graphql queries added

* integrate with api

* [autofix.ci] apply automated fixes

* ui polishment

* style and color polishment

* [autofix.ci] apply automated fixes

* style polishment

* [autofix.ci] apply automated fixes

* change status icon

* add unit to the job run duration

* [autofix.ci] apply automated fixes

* add back button in the job detail page

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
  • Loading branch information
wwayne and autofix-ci[bot] authored Mar 29, 2024
1 parent cad0cf4 commit 92224a6
Show file tree
Hide file tree
Showing 10 changed files with 426 additions and 239 deletions.
104 changes: 80 additions & 24 deletions ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/job-detail.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
'use client'

import React from 'react'
import { useSearchParams } from 'next/navigation'
import { useRouter, useSearchParams } from 'next/navigation'
import Ansi from '@curvenote/ansi-to-react'
import humanizerDuration from 'humanize-duration'
import moment from 'moment'
import { useQuery } from 'urql'

import { listJobRuns } from '@/lib/tabby/query'
import { cn } from '@/lib/utils'
import { IconAlertTriangle, IconTerminalSquare } from '@/components/ui/icons'
import {
IconAlertTriangle,
IconChevronLeft,
IconClock,
IconHistory,
IconStopWatch,
IconTerminalSquare
} from '@/components/ui/icons'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { ListSkeleton } from '@/components/skeleton'

import { JobsTable } from './jobs-table'
import { getLabelByExitCode } from '../utils/state'

export default function JobRunDetail() {
const router = useRouter()
const searchParams = useSearchParams()
const id = searchParams.get('id')
const [{ data, error, fetching }, reexecuteQuery] = useQuery({
Expand Down Expand Up @@ -46,27 +56,73 @@ export default function JobRunDetail() {
<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>
{currentNode && (
<>
<div
onClick={() => router.back()}
className="-ml-1 flex cursor-pointer items-center transition-opacity hover:opacity-60"
>
<IconChevronLeft className="mr-1 h-6 w-6" />
<h2 className="scroll-m-20 text-3xl font-bold tracking-tight first:mt-0">
{currentNode.job}
</h2>
</div>
<div className="mb-8 flex gap-x-5 text-sm text-muted-foreground lg:gap-x-10">
<div className="flex items-center gap-1">
<IconStopWatch />
<p>State: {getLabelByExitCode(currentNode.exitCode)}</p>
</div>

{currentNode.createdAt && (
<div className="flex items-center gap-1">
<IconClock />
<p>
Started:{' '}
{moment(currentNode.createdAt).format('YYYY-MM-DD HH:mm')}
</p>
</div>
)}

{currentNode.createdAt && currentNode.finishedAt && (
<div className="flex items-center gap-1">
<IconHistory />
<p>
Duration:{' '}
{humanizerDuration(
moment
.duration(
moment(currentNode.finishedAt).diff(
currentNode.createdAt
)
)
.asMilliseconds()
)}
</p>
</div>
)}
</div>
<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>
)}
</>
Expand Down
61 changes: 0 additions & 61 deletions ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/job-list.tsx

This file was deleted.

207 changes: 207 additions & 0 deletions ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/job-row.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
'use client'

import { useMemo } from 'react'
import Link from 'next/link'
import humanizerDuration from 'humanize-duration'
import { isNil } from 'lodash-es'
import moment from 'moment'
import { useQuery } from 'urql'

import { listJobRuns, queryJobRunStats } from '@/lib/tabby/query'
import { cn } from '@/lib/utils'
import { TableCell, TableRow } from '@/components/ui/table'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from '@/components/ui/tooltip'
import LoadingWrapper from '@/components/loading-wrapper'
import { ListRowSkeleton } from '@/components/skeleton'

function JobAggregateState({
count,
activeClass,
tooltip
}: {
count?: number
activeClass: string
tooltip: string
}) {
return (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger>
<div
className={cn(
'flex h-8 w-8 cursor-default items-center justify-center rounded-full',
{
[activeClass]: count,
'bg-muted text-muted': !count
}
)}
>
{count || ''}
</div>
</TooltipTrigger>
<TooltipContent>
<p>{tooltip}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}

function JobRunState({ name }: { name: string }) {
const [{ data, fetching }] = useQuery({
query: queryJobRunStats,
variables: {
jobs: [name]
}
})
return (
<LoadingWrapper
loading={fetching}
fallback={<ListRowSkeleton className="w-1/3" />}
>
<div className="flex items-center gap-3">
<JobAggregateState
count={data?.jobRunStats.success}
activeClass="bg-green-700 text-xs text-white"
tooltip="Success"
/>
<JobAggregateState
count={data?.jobRunStats.pending}
activeClass="bg-blue-700 text-xs text-white"
tooltip="Pending"
/>
<JobAggregateState
count={data?.jobRunStats.failed}
activeClass="bg-red-700 text-xs text-white"
tooltip="Failed"
/>
</div>
</LoadingWrapper>
)
}

export default function JobRow({ name }: { name: string }) {
const RECENT_DISPLAYED_SIZE = 10

const [{ data, fetching }] = useQuery({
query: listJobRuns,
variables: {
last: RECENT_DISPLAYED_SIZE,
jobs: [name]
}
})

const edges = data?.jobRuns?.edges
const displayJobs = useMemo(() => {
return edges?.slice().reverse()
}, [edges])
const lastJob = displayJobs?.[0]
const lastFinishedJob = displayJobs?.find(job => Boolean(job.node.finishedAt))
const lastSuccessAt = lastFinishedJob
? moment(lastFinishedJob.node.finishedAt).format('YYYY-MM-DD HH:mm')
: null
return (
<LoadingWrapper
loading={fetching}
fallback={
<TableRow>
<TableCell colSpan={4}>
<ListRowSkeleton />
</TableCell>
</TableRow>
}
>
<TableRow className="h-16">
<TableCell className="font-bold">{name}</TableCell>
<TableCell>
<div className="grid grid-cols-5 flex-wrap gap-1.5 xl:flex">
{displayJobs?.map(job => {
const { createdAt, finishedAt } = job.node
const startAt =
createdAt && moment(createdAt).format('YYYY-MM-DD HH:mm')
const duration: string | null =
(createdAt &&
finishedAt &&
humanizerDuration.humanizer({
language: 'shortEn',
languages: {
shortEn: {
d: () => 'd',
h: () => 'h',
m: () => 'm',
s: () => 's'
}
}
})(
moment
.duration(moment(finishedAt).diff(createdAt))
.asMilliseconds(),
{
units: ['d', 'h', 'm', 's'],
round: true,
largest: 1,
spacer: '',
language: 'shortEn'
}
)) ??
null

let displayedDuration = ''
if (duration !== null) {
const isSecond = duration.endsWith('s')
if (isSecond) {
displayedDuration = duration
} else {
const unit = duration.charAt(duration.length - 1)
const roundNumber = parseInt(duration) + 1
displayedDuration = roundNumber + unit
}
}

return (
<TooltipProvider delayDuration={0} key={job.node.id}>
<Tooltip>
<TooltipTrigger asChild>
<Link
href={`/jobs/detail?id=${job.node.id}`}
className={cn(
'flex h-8 w-8 items-center justify-center rounded text-xs text-white hover:opacity-70',
{
'bg-blue-700': isNil(job.node.exitCode),
'bg-green-700': job.node.exitCode === 0,
'bg-red-700': job.node.exitCode === 1
}
)}
>
{displayedDuration}
</Link>
</TooltipTrigger>
<TooltipContent>
{startAt && <p>{startAt}</p>}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
})}
</div>
</TableCell>
<TableCell>
<Link
href={`/jobs/detail?id=${lastJob?.node.id}`}
className="flex items-center underline"
>
<p>{lastSuccessAt}</p>
</Link>
</TableCell>
<TableCell>
<JobRunState name={name} />
</TableCell>
</TableRow>
</LoadingWrapper>
)
}
Loading

0 comments on commit 92224a6

Please sign in to comment.