Skip to content

Commit

Permalink
feat(dashboard): new tab display w/ SelectionLink
Browse files Browse the repository at this point in the history
  • Loading branch information
bryson-g committed Dec 14, 2024
1 parent c2960a4 commit 36a5d83
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 13 deletions.
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 '../../../../../../../../sdk-js/dist/proto/scheduled_wf_run'

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,64 @@
import { ScheduledWfRunIdList, WfSpec } from 'littlehorse-client/proto'
import { getScheduleWfSpec } from '../actions/getScheduleWfSpec'
import { SelectionLink } from '@/app/[tenantId]/components/SelectionLink'
import { ScheduledWfRun } from '../../../../../../../../sdk-js/dist/proto/scheduled_wf_run'
import { FUTURE_TIME_RANGES, TimeRange } from '@/app/constants'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { ClockIcon } from 'lucide-react'
import { useEffect, useState } from 'react'
import { getCronTimeWindow } from '@/app/utils/getCronTimeWindow'
import { parseExpression } from 'cron-parser'
import { utcToLocalDateTime } from '@/app/utils'
import { SearchVariableDialog } from './SearchVariableDialog'

export const ScheduledWfRuns = ({ scheduledWfRuns, spec }: { scheduledWfRuns: ScheduledWfRun[]; spec: WfSpec }) => {
const [currentWindow, setWindow] = useState<TimeRange>(-1)
const [filteredScheduledWfRuns, setFilteredScheduledWfRuns] = useState<ScheduledWfRun[]>(scheduledWfRuns)

useEffect(() => {
setFilteredScheduledWfRuns(
scheduledWfRuns.filter(scheduledWfRun => {
if (currentWindow === -1) return true

const timeWindow = getCronTimeWindow(scheduledWfRun.cronExpression)
return timeWindow && timeWindow <= currentWindow
})
)
}, [currentWindow, scheduledWfRuns])

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>
{/* {Object.keys(spec.threadSpecs).flatMap(threadSpec =>
spec.threadSpecs[threadSpec].variableDefs.filter(variableDef => variableDef.searchable)
).length > 0 && <SearchVariableDialog spec={spec} />} */}
</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
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,19 +2,23 @@
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 '../../../../../../../../sdk-js/dist/proto/scheduled_wf_run'

type WfSpecProps = {
spec: Spec
ScheduleWfSpec: ScheduledWfRunIdList
ScheduleWfSpec: ScheduledWfRun[]
}
export const WfSpec: FC<WfSpecProps> = ({ spec }) => {
export const WfSpec: FC<WfSpecProps> = ({ spec, ScheduleWfSpec }) => {
const { setModal, setShowModal } = useModal()
console.log(ScheduleWfSpec)

const onClick = useCallback(() => {
if (!spec) return
Expand All @@ -37,7 +41,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={spec} scheduledWfRuns={ScheduleWfSpec} />
</TabsContent>
</Tabs>
</>
)
}
14 changes: 14 additions & 0 deletions dashboard/src/app/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,17 @@ export const TIME_RANGES_NAMES: { [key in TimeRange]: string } = {
1440: '1 day',
4320: '3 days',
}

export const FUTURE_TIME_RANGES = [
{ label: 'All time', value: -1 },
{ label: 'Next 5 minutes', value: 5 },
{ label: 'Next 15 minutes', value: 15 },
{ label: 'Next 30 minutes', value: 30 },
{ label: 'Next 1 hour', value: 60 },
{ label: 'Next 3 hours', value: 180 },
{ label: 'Next 6 hours', value: 360 },
{ label: 'Next 12 hours', value: 720 },
{ label: 'Next 24 hours', value: 1440 },
{ label: 'Next 3 days', value: 4320 },
{ label: 'Next 7 days', value: 10080 },
] as const satisfies { label: string; value: number }[]
18 changes: 18 additions & 0 deletions dashboard/src/app/utils/getCronTimeWindow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { parseExpression } from 'cron-parser'

import { FUTURE_TIME_RANGES } from '@/app/constants'
export function getCronTimeWindow(cronExpression: string): number | undefined {
try {
const interval = parseExpression(cronExpression)
const nextExecution = interval.next().toDate()
const now = new Date()
const minutesUntil = (nextExecution.getTime() - now.getTime()) / (1000 * 60)

const timeWindow = FUTURE_TIME_RANGES.find(range => minutesUntil <= range.value)

return timeWindow?.value ?? undefined
} catch (error) {
console.error('Invalid cron expression:', error)
return undefined
}
}
55 changes: 55 additions & 0 deletions dashboard/src/components/ui/tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
'use client'

import * as React from 'react'
import * as TabsPrimitive from '@radix-ui/react-tabs'

import { cn } from '@/components/utils'

const Tabs = TabsPrimitive.Root

const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
'inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground',
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName

const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm',
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName

const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName

export { Tabs, TabsList, TabsTrigger, TabsContent }

0 comments on commit 36a5d83

Please sign in to comment.