Skip to content

Commit

Permalink
feat(dashboard): display scheduledWfRuns (#1202)
Browse files Browse the repository at this point in the history
  • Loading branch information
bryson-g authored Dec 19, 2024
1 parent 8a82d0a commit 3807fad
Show file tree
Hide file tree
Showing 15 changed files with 348 additions and 53 deletions.
53 changes: 53 additions & 0 deletions dashboard/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.1",
"@tanstack/react-query": "^5.37.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"cron-parser": "^4.9.0",
"dagre": "^0.8.5",
"littlehorse-client": "file://../sdk-js",
"lucide-react": "^0.379.0",
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
'use server'

import { lhClient } from '@/app/lhClient'
import { WithTenant } from '@/types'
import { ScheduledWfRunIdList } from 'littlehorse-client/proto'
import { ScheduledWfRun } from 'littlehorse-client/proto'

type GetWfSpecProps = {
name: string
version: string
} & WithTenant

export const getScheduleWfSpec = async ({ name, version, tenantId }: GetWfSpecProps): Promise<ScheduledWfRunIdList> => {
export const getScheduleWfSpec = async ({ name, version, tenantId }: GetWfSpecProps): Promise<ScheduledWfRun[]> => {
const client = await lhClient({ tenantId })

const [majorVersion, revision] = version.split('.')
return client.searchScheduledWfRun({
wfSpecName: name,
majorVersion: parseInt(majorVersion) || 0,
revision: parseInt(revision) | 0,
})

return Promise.all(
(
await client.searchScheduledWfRun({
wfSpecName: name,
majorVersion: parseInt(majorVersion) || 0,
revision: parseInt(revision) || 0,
})
).results.map(async scheduledWfRun => {
return await client.getScheduledWfRun({
id: scheduledWfRun.id,
})
})
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
'use client'

import { ScheduledWfRunIdList, WfSpec } from 'littlehorse-client/proto'
import { getScheduleWfSpec } from '../actions/getScheduleWfSpec'
import { SelectionLink } from '@/app/[tenantId]/components/SelectionLink'
import { ScheduledWfRun } from 'littlehorse-client/proto'
import { FUTURE_TIME_RANGES, SEARCH_DEFAULT_LIMIT, TimeRange } from '@/app/constants'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { ClockIcon } from 'lucide-react'
import { useEffect, useState, useMemo } from 'react'
import { getCronTimeWindow } from '@/app/utils/getCronTimeWindow'
import { parseExpression } from 'cron-parser'
import { utcToLocalDateTime } from '@/app/utils'
import { SearchVariableDialog } from './SearchVariableDialog'
import { SearchFooter } from '@/app/[tenantId]/components/SearchFooter'
import { useParams, useSearchParams } from 'next/navigation'
import { RefreshCwIcon } from 'lucide-react'

export const ScheduledWfRuns = (spec: WfSpec) => {
const [currentWindow, setWindow] = useState<TimeRange>(-1)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<Error | null>(null)
const [scheduledWfRuns, setScheduledWfRuns] = useState<ScheduledWfRun[]>([])
const tenantId = useParams().tenantId as string

useEffect(() => {
let isMounted = true

const fetchScheduledWfRuns = async () => {
try {
setIsLoading(true)
setError(null)
const runs = await getScheduleWfSpec({
name: spec.id!.name,
version: spec.id!.majorVersion + '.' + spec.id!.revision,
tenantId: tenantId,
})
if (isMounted) {
setScheduledWfRuns(runs)
}
} catch (err) {
if (isMounted) {
setError(err instanceof Error ? err : new Error('Failed to fetch scheduled runs'))
}
} finally {
if (isMounted) {
setIsLoading(false)
}
}
}

fetchScheduledWfRuns()

return () => {
isMounted = false
}
}, [spec.id, tenantId])

const filteredScheduledWfRuns = useMemo(
() =>
scheduledWfRuns
.filter(scheduledWfRun => {
if (currentWindow === -1) return true
const timeWindow = getCronTimeWindow(scheduledWfRun.cronExpression)
return timeWindow && timeWindow <= currentWindow
})
.sort((a, b) => {
const timeA = parseExpression(a.cronExpression).next().toDate().getTime()
const timeB = parseExpression(b.cronExpression).next().toDate().getTime()
return timeA - timeB
}),
[currentWindow, scheduledWfRuns]
)

if (isLoading) {
return (
<div className="flex min-h-[500px] items-center justify-center">
<RefreshCwIcon className="h-8 w-8 animate-spin text-blue-500" />
</div>
)
}

if (error) {
return (
<div className="flex min-h-[500px] flex-col items-center justify-center text-red-500">
<p>Error loading scheduled runs</p>
<p className="text-sm">{error.message}</p>
</div>
)
}

return (
<div className="flex min-h-[500px] flex-col">
<div className="flex gap-4">
<Select value={currentWindow.toString()} onValueChange={value => setWindow(parseInt(value) as TimeRange)}>
<SelectTrigger className="w-[150px] min-w-fit">
<div className="flex items-center gap-2">
<ClockIcon className="h-5 w-5 fill-none stroke-black" />
<SelectValue>{FUTURE_TIME_RANGES.find(time => time.value === currentWindow)?.label}</SelectValue>
</div>
</SelectTrigger>
<SelectContent>
{FUTURE_TIME_RANGES.map(time => (
<SelectItem key={time.value} value={time.value.toString()}>
{time.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="mt-4 flex flex-col">
{filteredScheduledWfRuns.map(scheduledWfRun => (
<SelectionLink aria-disabled key={scheduledWfRun.id?.id} href={undefined}>
<p>{scheduledWfRun.id?.id}</p>
<div className="flex items-center gap-2 rounded-md bg-gray-200 p-1 text-sm">
<ClockIcon className="h-5 w-5 fill-none stroke-black" />
<p>{utcToLocalDateTime(parseExpression(scheduledWfRun.cronExpression).next().toDate().toISOString())}</p>
</div>
</SelectionLink>
))}
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
'use client'
import LinkWithTenant from '@/app/[tenantId]/components/LinkWithTenant'
import { SearchFooter } from '@/app/[tenantId]/components/SearchFooter'
import { SEARCH_DEFAULT_LIMIT, TIME_RANGES, TimeRange } from '@/app/constants'
import { concatWfRunIds } from '@/app/utils'
Expand All @@ -10,6 +9,7 @@ import { useParams, useSearchParams } from 'next/navigation'
import { FC, Fragment, useMemo, useState } from 'react'
import { PaginatedWfRunIdList, searchWfRun } from '../actions/searchWfRun'
import { WfRunsHeader } from './WfRunsHeader'
import { SelectionLink } from '@/app/[tenantId]/components/SelectionLink'

export const WfRuns: FC<WfSpec> = spec => {
const searchParams = useSearchParams()
Expand Down Expand Up @@ -57,18 +57,13 @@ export const WfRuns: FC<WfSpec> = spec => {
<RefreshCwIcon className="h-8 w-8 animate-spin text-blue-500" />
</div>
) : (
<div className="flex min-h-[360px] flex-col gap-4">
<div className="flex min-h-[360px] flex-col">
{data?.pages.map((page, i) => (
<Fragment key={i}>
{page.results.map(wfRunId => (
<div key={wfRunId.id}>
<LinkWithTenant
className="py-2 text-blue-500 hover:underline"
href={`/wfRun/${concatWfRunIds(wfRunId)}`}
>
{wfRunId.id}
</LinkWithTenant>
</div>
<SelectionLink key={wfRunId.id} href={`/wfRun/${concatWfRunIds(wfRunId)}`}>
<p>{wfRunId.id}</p>
</SelectionLink>
))}
</Fragment>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ export const WfRunsHeader: FC<Props> = ({ spec, currentStatus, currentWindow, se

return (
<div className="mb-4 flex items-center justify-between">
<h2 className="text-2xl font-bold">WfRun Search</h2>

<div className="flex items-center justify-between gap-4">
<Select value={currentWindow.toString()} onValueChange={value => setWindow(parseInt(value) as TimeRange)}>
<SelectTrigger className="w-[150px] min-w-fit">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@
import { Navigation } from '@/app/[tenantId]/components/Navigation'
import { ScheduledWfRunIdList, WfSpec as Spec } from 'littlehorse-client/proto'
import { LucidePlayCircle } from 'lucide-react'
import { FC, useCallback } from 'react'
import { FC, useCallback, useState } from 'react'
import { Diagram } from '../../../components/Diagram'
import { useModal } from '../../../hooks/useModal'
import { Details } from './Details'
import { Thread } from './Thread'
import { WfRuns } from './WfRuns'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { ScheduledWfRuns } from './ScheduledWfRuns'
import { ScheduledWfRun } from 'littlehorse-client/proto'

type WfSpecProps = {
spec: Spec
ScheduleWfSpec: ScheduledWfRunIdList
}
export const WfSpec: FC<WfSpecProps> = ({ spec }) => {
const { setModal, setShowModal } = useModal()
Expand All @@ -37,7 +39,19 @@ export const WfSpec: FC<WfSpecProps> = ({ spec }) => {
.map(name => (
<Thread key={name} name={name} spec={spec.threadSpecs[name]} />
))}
<WfRuns {...spec} />

<Tabs defaultValue="runs">
<TabsList>
<TabsTrigger value="runs">WfRuns</TabsTrigger>
<TabsTrigger value="schedule">ScheduledWfRuns</TabsTrigger>
</TabsList>
<TabsContent value="runs">
<WfRuns {...spec} />
</TabsContent>
<TabsContent value="schedule">
<ScheduledWfRuns {...spec} />
</TabsContent>
</Tabs>
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ export default async function Page({ params: { props, tenantId } }: Props) {

try {
const wfSpec = await getWfSpec({ tenantId, name, version })
const scheduleWfSpec = await getScheduleWfSpec({ tenantId, name, version })
return <WfSpec spec={wfSpec} ScheduleWfSpec={scheduleWfSpec} />
return <WfSpec spec={wfSpec} />
} catch (error) {
if (error instanceof ClientError && error.code === Status.NOT_FOUND) return notFound()
throw error
Expand Down
9 changes: 6 additions & 3 deletions dashboard/src/app/[tenantId]/components/LinkWithTenant.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ import NextLink from 'next/link'
import { useParams } from 'next/navigation'
import { ComponentProps } from 'react'

const LinkWithTenant = ({ linkStyle, ...props }: ComponentProps<typeof NextLink> & { linkStyle?: boolean }) => {
const LinkWithTenant = ({ linkStyle, href, ...props }: ComponentProps<typeof NextLink> & { linkStyle?: boolean }) => {
const { tenantId } = useParams()

return (
<NextLink
{...props}
href={`/${tenantId}${props.href}`}
className={cn(props.className, { 'text-blue-500 underline': linkStyle })}
href={`/${tenantId}${href}`}
className={cn(props.className, {
'[&>*:first-child]:text-blue-500 [&>*:first-child]:hover:underline': linkStyle,
})}
/>
)
}
Expand Down
Loading

0 comments on commit 3807fad

Please sign in to comment.