Fix: UI Bug Burndown, Part II (#1788)

* fix: hard redirect

* fix: set value

* fix: drill

* fix: start time default bug

* fix: only show worker filter if there are active workers

* fix: rm deps from callbacks

* fix: worker filtering

* hack: events runs on hover

* feat: badge ui

* fix: remove event detail page

* fix: rm cruft

* fix: loading state

* fix: initial side sheet rework

* feat: initial work un-borking the side sheet

* fix: more ui

* fix: so close!

* fix: fixed height for main view

* fix: height

* fix: race

* feat: improved handle

* fix: docs

* fix: close the side sheet on opening docs

* fix: close docs on side sheet open

* chore: lint

* fix: doc sheet

* fix: route

* feat: persist panel width

* feat: progress on combining docs and detail sheets

* feat: wire up docs

* feat: clean up some dead code, improve typing

* fix: more layout tweaks

* fix: more misc. things

* fix: side panel

* fix: rm more dead code

* fix: rm duped use of sheet + docs providers

* fix: worker detail

* fix: remove side sheet!

* fix: remove a ton more dead code (detailWithSheet thing)

* fix: use docs button on sidebar

* feat: remove the rest of the docs code

* fix: attempts

* chore: gen

* fix: don't run tests on FE changes

* Fix  bug burndown part iii (#1789)

* fix: hard redirect

* fix: set value

* fix: drill

* fix: start time default bug

* fix: only show worker filter if there are active workers

* fix: rm deps from callbacks

* fix: worker filtering

* fix: auth

* redirect issue

* empty state and layout

* trigger bug fixes

* empty state

---------

Co-authored-by: mrkaye97 <mrkaye97@gmail.com>

---------

Co-authored-by: Gabe Ruttner <gabriel.ruttner@gmail.com>
This commit is contained in:
Matt Kaye
2025-06-02 11:19:19 -04:00
committed by GitHub
parent 7f0961b022
commit 9630bf5a7d
39 changed files with 1230 additions and 1460 deletions

View File

@@ -3,7 +3,7 @@ on:
pull_request:
paths-ignore:
- "sdks/**"
- "frontend/docs/**"
- "frontend/**"
- "examples/**"
jobs:

View File

@@ -3,4 +3,4 @@ from hatchet_sdk import Hatchet
hatchet = Hatchet()
# > Event trigger
hatchet.event.push("user:create", {})
hatchet.event.push("user:create", {"should_skip": False})

View File

@@ -44,16 +44,12 @@ import {
FilterSelect,
} from '@/next/components/ui/filters/filters';
import { useFilters } from '@/next/hooks/utils/use-filters';
import { ROUTES } from '@/next/lib/routes';
import { FilterProvider } from '@/next/hooks/utils/use-filters';
interface RunEventLogProps {
filters?: ActivityFilters;
workflow: V1WorkflowRun;
onTaskSelect?: (
event: V1TaskEvent,
options?: Parameters<typeof ROUTES.runs.detailWithSheet>[3],
) => void;
onTaskSelect?: (event: V1TaskEvent) => void;
showFilters?: {
search?: boolean;
eventType?: boolean;
@@ -305,7 +301,7 @@ const EventMessage = ({ event, onTaskSelect }: EventMessageProps) => {
className="h-5 p-1 text-xs text-muted-foreground hover:text-muted-foreground/80 border-muted-foreground/50"
onClick={(e) => {
e.stopPropagation();
onTaskSelect(event, { taskTab: 'worker' });
onTaskSelect(event);
}}
>
<CpuIcon className="h-3 w-3" />
@@ -525,7 +521,7 @@ function RunEventLogContent({
]);
return (
<div className="space-y-2">
<div className="space-y-2 h-full">
<FilterGroup>
{showFilters.search && (
<FilterText<ActivityFilters>

View File

@@ -37,17 +37,7 @@ export function RunId({
? ROUTES.runs.detail(tenantId, wfRun?.metadata.id || id || '')
: taskRun?.type == V1WorkflowType.TASK
? undefined
: ROUTES.runs.detailWithSheet(
tenantId,
taskRun?.workflowRunExternalId || '',
{
type: 'task-detail',
props: {
selectedWorkflowRunId: taskRun?.workflowRunExternalId || '',
selectedTaskId: taskRun?.taskExternalId,
},
},
);
: ROUTES.runs.detail(tenantId, taskRun?.workflowRunExternalId || '');
const displayNameIdPrefix = splitTime(displayName);
const friendlyDisplayName = displayNameIdPrefix || displayName;

View File

@@ -23,14 +23,19 @@ import { ROUTES } from '@/next/lib/routes';
import { useRuns } from '@/next/hooks/use-runs';
import { Checkbox } from '@/next/components/ui/checkbox';
import { Button } from '@/next/components/ui/button';
import { ChevronDownIcon, ChevronRightIcon, Drill } from 'lucide-react';
import {
ArrowDownFromLine,
ChevronDownIcon,
ChevronRightIcon,
} from 'lucide-react';
import { cn } from '@/next/lib/utils';
export const columns = (
rowClicked?: (row: V1TaskSummary) => void,
selectAll?: boolean,
): ColumnDef<V1TaskSummary>[] => [
{
allowSelection: boolean = true,
): ColumnDef<V1TaskSummary>[] => {
const selectCheckboxColumn: ColumnDef<V1TaskSummary> = {
id: 'select',
header: ({ table }) => (
<Checkbox
@@ -79,166 +84,173 @@ export const columns = (
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: 'status',
header: ({ column }) => <DataTableColumnHeader column={column} title="" />,
cell: ({ row }) => {
const status = row.getValue('status') as V1TaskStatus;
return (
<div className="flex items-center justify-center h-full">
<RunsBadge variant="xs" status={status} />
</div>
);
},
filterFn: (row, id, value) => {
return value.includes(row.getValue(id));
},
enableSorting: false,
enableHiding: false,
},
{
accessorKey: 'runId',
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Run ID" />
),
cell: ({ row }) => {
const url = ROUTES.runs.detailWithSheet(
row.original.tenantId,
row.original.workflowRunExternalId || '',
{
type: 'task-detail',
props: {
selectedWorkflowRunId: row.original.workflowRunExternalId || '',
selectedTaskId: row.original.taskExternalId,
},
},
);
};
return (
<div className="flex items-center gap-2">
<RunId
taskRun={row.original}
onClick={() => {
rowClicked?.(row.original);
}}
/>
{url && (
<Link
to={url}
className="opacity-0 group-hover:opacity-100 transition-opacity text-muted-foreground hover:text-foreground"
onClick={(e) => e.stopPropagation()}
>
<Button variant="link" tooltip="Drill down into run" size="icon">
<Drill className="w-4 h-4" />
</Button>
</Link>
)}
</div>
);
const contentColumns: ColumnDef<V1TaskSummary>[] = [
{
accessorKey: 'status',
header: ({ column }) => (
<DataTableColumnHeader column={column} title="" />
),
cell: ({ row }) => {
const status = row.getValue('status') as V1TaskStatus;
return (
<div className="flex items-center justify-center h-full">
<RunsBadge variant="xs" status={status} />
</div>
);
},
filterFn: (row, id, value) => {
return value.includes(row.getValue(id));
},
enableSorting: false,
enableHiding: false,
},
enableSorting: false,
enableHiding: false,
},
{
accessorKey: 'workflowName',
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Definition" />
),
cell: ({ row }) => <div>{row.getValue('workflowName')}</div>,
enableSorting: false,
enableHiding: false,
},
{
accessorKey: 'createdAt',
header: ({ column }) => (
<DataTableColumnHeader
column={column}
title="Created"
orderBy={WorkflowRunOrderByField.CreatedAt}
/>
),
cell: ({ row }) => (
<Time
date={row.getValue('createdAt')}
variant="timeSince"
tooltipVariant="timestamp"
/>
),
enableSorting: false,
enableHiding: true,
},
{
accessorKey: 'startedAt',
header: ({ column }) => (
<DataTableColumnHeader
column={column}
title="Started"
orderBy={WorkflowRunOrderByField.StartedAt}
/>
),
cell: ({ row }) => {
const startedAt = row.getValue('startedAt') as string | null;
if (!startedAt) {
return <span>-</span>;
}
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span>
<Time date={startedAt} variant="timeSince" />
</span>
</TooltipTrigger>
<TooltipContent className="bg-muted">
<Time
date={startedAt}
variant="timestamp"
asChild
className="font-mono text-foreground"
/>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
{
accessorKey: 'runId',
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Run ID" />
),
cell: ({ row }) => {
const url = ROUTES.runs.detail(
row.original.tenantId,
row.original.workflowRunExternalId || '',
);
return (
<div className="flex items-center gap-2">
<RunId
taskRun={row.original}
onClick={() => {
rowClicked?.(row.original);
}}
/>
{url && (
<Link
to={url}
className="opacity-0 group-hover:opacity-100 transition-opacity text-muted-foreground hover:text-foreground"
onClick={(e) => e.stopPropagation()}
>
<Button
variant="link"
tooltip="Drill down into run"
size="icon"
>
<ArrowDownFromLine className="w-4 h-4" />
</Button>
</Link>
)}
</div>
);
},
enableSorting: false,
enableHiding: false,
},
enableSorting: false,
enableHiding: true,
},
{
accessorKey: 'duration',
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Duration" />
),
cell: ({ row }) => {
const startedAt = row.getValue('startedAt') as string | null;
const finishedAt = row.original.finishedAt as string | null;
const status = row.getValue('status') as V1TaskStatus;
return (
<Duration
start={startedAt}
end={finishedAt}
status={status}
variant="compact"
{
accessorKey: 'workflowName',
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Definition" />
),
cell: ({ row }) => <div>{row.getValue('workflowName')}</div>,
enableSorting: false,
enableHiding: false,
},
{
accessorKey: 'createdAt',
header: ({ column }) => (
<DataTableColumnHeader
column={column}
title="Created"
orderBy={WorkflowRunOrderByField.CreatedAt}
/>
);
),
cell: ({ row }) => (
<Time
date={row.getValue('createdAt')}
variant="timeSince"
tooltipVariant="timestamp"
/>
),
enableSorting: false,
enableHiding: true,
},
enableSorting: false,
enableHiding: true,
},
{
accessorKey: 'additionalMetadata',
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Metadata" />
),
cell: ({ row }) => {
return <AdditionalMetadataCell row={row} />;
{
accessorKey: 'startedAt',
header: ({ column }) => (
<DataTableColumnHeader
column={column}
title="Started"
orderBy={WorkflowRunOrderByField.StartedAt}
/>
),
cell: ({ row }) => {
const startedAt = row.getValue('startedAt') as string | null;
if (!startedAt) {
return <span>-</span>;
}
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span>
<Time date={startedAt} variant="timeSince" />
</span>
</TooltipTrigger>
<TooltipContent className="bg-muted">
<Time
date={startedAt}
variant="timestamp"
asChild
className="font-mono text-foreground"
/>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
},
enableSorting: false,
enableHiding: true,
},
enableSorting: false,
enableHiding: true,
},
];
{
accessorKey: 'duration',
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Duration" />
),
cell: ({ row }) => {
const startedAt = row.getValue('startedAt') as string | null;
const finishedAt = row.original.finishedAt as string | null;
const status = row.getValue('status') as V1TaskStatus;
return (
<Duration
start={startedAt}
end={finishedAt}
status={status}
variant="compact"
/>
);
},
enableSorting: false,
enableHiding: true,
},
{
accessorKey: 'additionalMetadata',
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Metadata" />
),
cell: ({ row }) => {
return <AdditionalMetadataCell row={row} />;
},
enableSorting: false,
enableHiding: true,
},
];
if (allowSelection) {
return [selectCheckboxColumn, ...contentColumns];
}
return contentColumns;
};
function AdditionalMetadataCell({ row }: { row: Row<V1TaskSummary> }) {
const {

View File

@@ -33,6 +33,9 @@ interface RunsTableProps {
onSelectionChange?: (selectedRows: V1TaskSummary[]) => void;
onTriggerRunClick?: () => void;
excludedFilters?: (keyof RunsFilters)[];
showPagination?: boolean;
allowSelection?: boolean;
showActions?: boolean;
}
export function RunsTable({
@@ -41,6 +44,9 @@ export function RunsTable({
onSelectionChange,
onTriggerRunClick,
excludedFilters = [],
showPagination = true,
allowSelection = true,
showActions = true,
}: RunsTableProps) {
const {
data: runs,
@@ -144,15 +150,7 @@ export function RunsTable({
const handleRowDoubleClick = useCallback(
(row: V1TaskSummary) => {
navigate(
ROUTES.runs.detailWithSheet(tenantId, row.workflowRunExternalId || '', {
type: 'task-detail',
props: {
selectedWorkflowRunId: row.workflowRunExternalId || '',
selectedTaskId: row.taskExternalId,
},
}),
);
navigate(ROUTES.runs.detail(tenantId, row.workflowRunExternalId || ''));
},
[navigate, tenantId],
);
@@ -236,7 +234,7 @@ export function RunsTable({
</span>
)}
{count > 0 && !selectAll && (
{count > 0 && !selectAll && allowSelection && (
<Button
variant="ghost"
size="sm"
@@ -257,66 +255,71 @@ export function RunsTable({
</Button>
)}
</div>
{!selectAll ? (
<div className="flex gap-2">
<Button
tooltip={
numSelectedRows == 0
? 'No runs selected'
: canReplay
? 'Replay the selected runs'
: 'Cannot replay the selected runs'
}
variant="outline"
size="sm"
disabled={!canReplay || replay.isPending}
onClick={async () => replay.mutateAsync({ tasks: selectedRuns })}
>
<MdOutlineReplay className="h-4 w-4" />
Replay
</Button>
<Button
tooltip={
numSelectedRows == 0
? 'No runs selected'
: canCancel
? 'Cancel the selected runs'
: 'Cannot cancel the selected runs because they are not running or queued'
}
variant="outline"
size="sm"
disabled={!canCancel || cancel.isPending}
onClick={async () => cancel.mutateAsync({ tasks: selectedRuns })}
>
<MdOutlineCancel className="h-4 w-4" />
Cancel
</Button>
</div>
) : (
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
disabled={replay.isPending}
onClick={() => setShowBulkActionDialog('replay')}
>
<MdOutlineReplay className="h-4 w-4" />
Replay All
</Button>
<Button
variant="outline"
size="sm"
disabled={cancel.isPending}
onClick={() => setShowBulkActionDialog('cancel')}
>
<MdOutlineCancel className="h-4 w-4" />
Cancel All
</Button>
</div>
)}
{showActions &&
(!selectAll ? (
<div className="flex gap-2">
<Button
tooltip={
numSelectedRows == 0
? 'No runs selected'
: canReplay
? 'Replay the selected runs'
: 'Cannot replay the selected runs'
}
variant="outline"
size="sm"
disabled={!canReplay || replay.isPending}
onClick={async () =>
replay.mutateAsync({ tasks: selectedRuns })
}
>
<MdOutlineReplay className="h-4 w-4" />
Replay
</Button>
<Button
tooltip={
numSelectedRows == 0
? 'No runs selected'
: canCancel
? 'Cancel the selected runs'
: 'Cannot cancel the selected runs because they are not running or queued'
}
variant="outline"
size="sm"
disabled={!canCancel || cancel.isPending}
onClick={async () =>
cancel.mutateAsync({ tasks: selectedRuns })
}
>
<MdOutlineCancel className="h-4 w-4" />
Cancel
</Button>
</div>
) : (
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
disabled={replay.isPending}
onClick={() => setShowBulkActionDialog('replay')}
>
<MdOutlineReplay className="h-4 w-4" />
Replay All
</Button>
<Button
variant="outline"
size="sm"
disabled={cancel.isPending}
onClick={() => setShowBulkActionDialog('cancel')}
>
<MdOutlineCancel className="h-4 w-4" />
Cancel All
</Button>
</div>
))}
</div>
<DataTable
columns={columns(onRowClick, selectAll)}
columns={columns(onRowClick, selectAll, allowSelection)}
data={runs || []}
onDoubleClick={handleRowDoubleClick}
emptyState={
@@ -355,10 +358,12 @@ export function RunsTable({
selectAll={selectAll}
getSubRows={(row) => row.children || []}
/>
<Pagination className="justify-between flex flex-row">
<PageSizeSelector />
<PageSelector variant="dropdown" />
</Pagination>
{showPagination && (
<Pagination className="justify-between flex flex-row">
<PageSizeSelector />
<PageSelector variant="dropdown" />
</Pagination>
)}
<RunsBulkActionDialog
open={!!showBulkActionDialog}

View File

@@ -94,9 +94,7 @@ function WithPreviousInput({
const { data: selectedRunDetails } = useRunDetail();
useEffect(() => {
if (selectedRunDetails?.run) {
setInput(
JSON.stringify((selectedRunDetails.run.input as any).input, null, 2),
);
setInput(JSON.stringify(selectedRunDetails.run.input, null, 2));
setAddlMeta(
JSON.stringify(selectedRunDetails.run.additionalMetadata, null, 2),
);
@@ -221,9 +219,7 @@ function TriggerRunModalContent({
data: {
input: inputObj,
additionalMetadata: addlMetaObj,
triggerAt: new Date(
scheduleTime.getTime() - scheduleTime.getTimezoneOffset() * 60000,
).toISOString(),
triggerAt: new Date(scheduleTime.getTime()).toISOString(),
},
},
{
@@ -346,24 +342,39 @@ function TriggerRunModalContent({
<FaCodeBranch className="text-muted-foreground" size={16} />
From Recent Run
</label>
<select
className="w-full mt-1 rounded-md border border-input bg-background px-3 py-2 disabled:opacity-50 disabled:cursor-not-allowed"
<Select
value={selectedRunId}
disabled={!selectedWorkflowId}
onChange={(e) => {
const runId = e.target.value;
setSelectedRunId(runId);
onValueChange={(value) => {
setSelectedRunId(value);
}}
>
<option value="">Select a recent run</option>
{recentRuns
?.filter((run) => run.workflowId === selectedWorkflowId)
.map((run) => (
<option key={run.metadata.id} value={run.metadata.id}>
{getFriendlyWorkflowRunId(run)}
</option>
))}
</select>
<SelectTrigger className="w-full mt-1">
<SelectValue placeholder="Select a recent run" />
</SelectTrigger>
<SelectContent>
{(() => {
const filteredRuns =
recentRuns?.filter(
(run) => run.workflowId === selectedWorkflowId,
) || [];
if (filteredRuns.length === 0) {
return (
<SelectItem value="no-runs" disabled>
No recent runs available
</SelectItem>
);
}
return filteredRuns.map((run) => (
<SelectItem key={run.metadata.id} value={run.metadata.id}>
{getFriendlyWorkflowRunId(run)}
</SelectItem>
));
})()}
</SelectContent>
</Select>
</div>
)}

View File

@@ -1,7 +1,6 @@
import * as React from 'react';
import { Button, ButtonProps } from './button';
import { BookOpenIcon } from 'lucide-react';
import { DocRef, useDocs } from '@/next/hooks/use-docs-sheet';
import { cn } from '@/next/lib/utils';
import {
Tooltip,
@@ -9,6 +8,12 @@ import {
TooltipProvider,
TooltipTrigger,
} from './tooltip';
import { useSidePanel } from '@/next/hooks/use-side-panel';
export type DocRef = {
title: string;
href: string;
};
interface DocsButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
@@ -20,7 +25,7 @@ interface DocsButtonProps
titleOverride?: string;
}
const baseDocsUrl = 'https://docs.hatchet.run';
export const baseDocsUrl = 'https://docs.hatchet.run';
export function DocsButton({
doc,
@@ -31,12 +36,19 @@ export function DocsButton({
titleOverride,
...props
}: DocsButtonProps) {
const { open } = useDocs();
const { close: closeSideSheet, open } = useSidePanel();
const handleClick = (e: React.MouseEvent) => {
if (method === 'sheet') {
e.preventDefault();
open(doc);
closeSideSheet();
open({
type: 'docs',
content: {
href: `${baseDocsUrl}${doc.href}`,
title: doc.title,
},
});
} else {
window.open(`${baseDocsUrl}${doc.href}`, '_blank');
}

View File

@@ -1,110 +0,0 @@
import { Sheet, SheetContent, SheetHeader, SheetTitle } from './sheet';
import { DocsSheet } from '@/next/hooks/use-docs-sheet';
import { useIsMobile } from '@/next/hooks/use-mobile';
import { Cross2Icon, ExternalLinkIcon } from '@radix-ui/react-icons';
interface DocsSheetProps {
sheet: DocsSheet;
onClose: () => void;
variant?: 'overlay' | 'push';
}
export function DocsSheetComponent({
sheet,
onClose,
variant = 'push',
}: DocsSheetProps) {
const isMobile = useIsMobile();
// If using push variant, render as a side panel instead of using Sheet
if (variant === 'push' && !isMobile) {
return (
<div
className={`
h-full min-h-screen bg-background border-l border-border
transition-all duration-300 ease-in-out
${sheet.isOpen ? 'lg:w-[500px] md:w-[350px] w-[250px]' : 'w-0 overflow-hidden'}
`}
>
{sheet.isOpen && (
<div className="h-full min-h-screen flex flex-col p-4 md:p-6 overflow-hidden">
<div className="flex justify-between items-center mb-4 shrink-0">
<h2 className="text-lg font-semibold truncate pr-2">
{sheet.title}
</h2>
<div className="flex items-center gap-2">
{sheet.url && (
<a
href={sheet.url}
target="_blank"
rel="noopener noreferrer"
className="rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 flex-shrink-0"
title="Open in new tab"
>
<ExternalLinkIcon className="h-4 w-4" />
<span className="sr-only">Open in new tab</span>
</a>
)}
<button
onClick={onClose}
className="rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 flex-shrink-0"
>
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</button>
</div>
</div>
<div className="flex-1 overflow-hidden relative">
{sheet.url && (
<iframe
src={sheet.url}
className="absolute inset-0 w-full h-full rounded-md border"
title={`Documentation: ${sheet.title}`}
loading="lazy"
/>
)}
</div>
</div>
)}
</div>
);
}
// Fall back to the overlay variant if not using push
return (
<Sheet open={sheet.isOpen} onOpenChange={(open) => !open && onClose()}>
<SheetContent
side="right"
className="p-4 md:p-6 w-[min(500px,90vw)] h-screen"
>
<SheetHeader className="mb-4 pr-8">
<div className="flex justify-between items-center">
<SheetTitle className="truncate">{sheet.title}</SheetTitle>
{sheet.url && (
<a
href={sheet.url}
target="_blank"
rel="noopener noreferrer"
className="rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 flex-shrink-0"
title="Open in new tab"
>
<ExternalLinkIcon className="h-4 w-4" />
<span className="sr-only">Open in new tab</span>
</a>
)}
</div>
</SheetHeader>
<div className="flex-1 relative overflow-hidden">
{sheet.url && (
<iframe
src={sheet.url}
className="absolute inset-0 w-full h-full rounded-md border"
title={`Documentation: ${sheet.title}`}
loading="lazy"
/>
)}
</div>
</SheetContent>
</Sheet>
);
}

View File

@@ -252,6 +252,11 @@ export function FilterWorkerSelect<T>({
...props
}: FilterWorkerSelectProps<T>) {
const { data: options = [] } = useWorkers();
if (!options.length) {
return null;
}
return (
<FilterSelect<T, string>
options={[...new Set(options.map((o) => o.name))].map((o) => ({

View File

@@ -1,158 +1,40 @@
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '../sheet';
import { useSideSheet } from '@/next/hooks/use-side-sheet';
import { useIsMobile } from '@/next/hooks/use-mobile';
import { Cross2Icon, ExternalLinkIcon } from '@radix-ui/react-icons';
import { RunDetailSheet } from '@/next/pages/authenticated/dashboard/runs/detail-sheet/run-detail-sheet';
import { useMemo, useCallback } from 'react';
import { cn } from '@/lib/utils';
import { WorkerDetails } from '@/next/pages/authenticated/dashboard/workers/components/worker-details';
import { useSidebar } from '@/next/components/ui/sidebar';
import { Cross2Icon } from '@radix-ui/react-icons';
import { Button } from '../button';
import { useSidePanel } from '@/next/hooks/use-side-panel';
interface SideSheetProps {
variant?: 'overlay' | 'push';
}
export function SidePanel() {
const { content: maybeContent, isOpen, close } = useSidePanel();
interface SideSheetContent {
component: React.ReactNode;
title: React.ReactNode;
actions?: React.ReactNode;
}
export function SideSheetComponent({ variant = 'push' }: SideSheetProps) {
const isMobile = useIsMobile();
const { toggleExpand, sheet, close } = useSideSheet();
const { isCollapsed } = useSidebar();
const isOpen = useMemo(() => !!sheet.openProps, [sheet.openProps]);
const onClose = useCallback(() => {
close();
}, [close]);
const content = useMemo<SideSheetContent | undefined>(() => {
if (sheet.openProps?.type === 'task-detail') {
return {
component: (
<RunDetailSheet
isOpen={isOpen}
onClose={onClose}
{...sheet.openProps.props}
/>
),
title: 'Run Detail',
actions: (
<>
{/* <a
href={sheet.openProps?.props.detailsLink}
target="_blank"
rel="noopener noreferrer"
className="rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 flex-shrink-0"
title="Open in new tab"
>
<ExternalLinkIcon className="h-4 w-4" />
<span className="sr-only">Open in new tab</span>
</a> */}
</>
),
};
}
if (sheet.openProps?.type === 'worker-detail') {
return {
component: <WorkerDetails {...sheet.openProps.props} />,
title: 'Worker Detail',
actions: (
<>
<a
// href={ROUTES.workerServices.detail(sheet.openProps?.props.serviceName, sheet.openProps?.props.workerId)}
target="_blank"
rel="noopener noreferrer"
className="rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 flex-shrink-0"
title="Open in new tab"
>
<ExternalLinkIcon className="h-4 w-4" />
<span className="sr-only">Open in new tab</span>
</a>
</>
),
};
}
return undefined;
}, [sheet, isOpen, onClose]);
// If using push variant, render as a side panel instead of using Sheet
if (variant === 'push' && !isMobile) {
return (
<div
className={`
h-full min-h-screen bg-background border-l border-border
transition-all duration-300 ease-in-out relative
${isOpen ? (sheet.isExpanded ? 'lg:w-[800px] md:w-[600px] w-[400px]' : 'lg:w-[500px] md:w-[350px] w-[250px]') : 'w-0 overflow-hidden'}
`}
>
{isOpen && (
<>
<button
onClick={toggleExpand}
className={cn(
'absolute inset-y-0 -left-2 z-20 w-4 transition-w ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-border',
sheet.isExpanded ? 'cursor-e-resize' : 'cursor-w-resize',
)}
title="Toggle width"
/>
<div className="h-full min-h-screen flex flex-col overflow-hidden">
<div
className={cn(
'flex justify-between items-center border-b shrink-0 transition-h duration-300',
isMobile
? 'h-16 px-4'
: isCollapsed
? 'h-12 px-8'
: 'h-16 px-8',
)}
>
<h2 className="text-lg font-semibold truncate pr-2">
{content?.title}
</h2>
<div className="flex items-center gap-2">
{content?.actions}
<button
onClick={onClose}
className="rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 flex-shrink-0"
>
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</button>
</div>
</div>
<div className="flex-1 overflow-y-auto relative">
{content?.component}
</div>
</div>
</>
)}
</div>
);
if (!maybeContent) {
return null;
}
// Fall back to the overlay variant if not using push
return (
<Sheet open={isOpen} onOpenChange={(open) => !open && onClose()}>
<SheetContent
side="right"
className={`p-4 md:p-6 ${isOpen ? (sheet.isExpanded ? 'w-[min(800px,90vw)]' : 'w-[min(500px,90vw)]') : 'w-0 overflow-hidden'} h-screen`}
>
<SheetHeader className="mb-4 pr-8">
<div className="flex justify-between items-center">
<SheetTitle className="truncate">{content?.title}</SheetTitle>
{content?.actions}
isOpen && (
<div className="flex flex-col h-screen">
<div
className={
'flex flex-row w-full justify-between items-center border-b bg-background h-16 px-4 md:px-12'
}
>
<h2 className="text-lg font-semibold truncate pr-2">
{maybeContent.title}
</h2>
<div className="flex items-center gap-2">
{!maybeContent.isDocs && maybeContent.actions}
<Button
variant="ghost"
onClick={close}
className="rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 flex-shrink-0"
>
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</Button>
</div>
</SheetHeader>
<div className="flex-1 relative overflow-hidden">
{content?.component}
</div>
</SheetContent>
</Sheet>
{maybeContent.component}
</div>
)
);
}

View File

@@ -157,6 +157,7 @@ const SidebarProvider = React.forwardRef<
<SidebarContext.Provider value={contextValue}>
<TooltipProvider delayDuration={0}>
<div
data-state={state}
style={
{
'--sidebar-width': SIDEBAR_WIDTH,
@@ -165,7 +166,7 @@ const SidebarProvider = React.forwardRef<
} as React.CSSProperties
}
className={cn(
'group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar',
`group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar data-[state=expanded]:pl-[var(--sidebar-width)] data-[state=collapsed]:pl-[var(--sidebar-width-icon)]`,
className,
)}
ref={ref}

View File

@@ -9,7 +9,12 @@ import {
ResponsiveContainer,
Cell,
} from 'recharts';
import { ChevronDown, ChevronRight, Drill, Loader } from 'lucide-react';
import {
ArrowDownFromLine,
ChevronDown,
ChevronRight,
Loader,
} from 'lucide-react';
import { ChartContainer, ChartTooltipContent } from '@/components/ui/chart';
import { V1TaskStatus, V1TaskTiming } from '@/lib/api';
@@ -633,7 +638,14 @@ export function Waterfall({
/>
);
},
[workflowRunId, selectedTaskId, handleBarClick, toggleTask, processedData],
[
workflowRunId,
selectedTaskId,
handleBarClick,
toggleTask,
processedData,
tenantId,
],
);
// Handle loading or error states
@@ -845,13 +857,7 @@ const Tick = ({
task.taskExternalId === workflowRunId &&
task.parentId && (
<Link
to={ROUTES.runs.detailWithSheet(tenantId, task.parentId, {
type: 'task-detail',
props: {
selectedWorkflowRunId: task.workflowRunId,
selectedTaskId: task.id,
},
})}
to={ROUTES.runs.detail(tenantId, task.parentId)}
onClick={(e) => e.stopPropagation()}
>
<Button
@@ -866,17 +872,7 @@ const Tick = ({
)}
{task.hasChildren && (
<Link
to={ROUTES.runs.detailWithSheet(
tenantId,
task.workflowRunId || task.id,
{
type: 'task-detail',
props: {
selectedWorkflowRunId: task.workflowRunId || task.id,
selectedTaskId: task.id,
},
},
)}
to={ROUTES.runs.detail(tenantId, task.workflowRunId || task.id)}
>
<Button
tooltip="Drill into child task"
@@ -885,7 +881,7 @@ const Tick = ({
className="group-hover:opacity-100 opacity-0 transition-opacity duration-200"
>
{' '}
<Drill className="w-4 h-4" />
<ArrowDownFromLine className="w-4 h-4" />
</Button>
</Link>
)}

View File

@@ -1,80 +0,0 @@
import { createContext, useContext, useState } from 'react';
import docMetadata from '@/next/lib/docs';
export const pages = docMetadata;
export type DocRef = {
title: string;
href: string;
};
export interface DocsSheet {
isOpen: boolean;
url: string;
title: string;
}
interface DocsContextValue {
sheet: DocsSheet;
open: (doc: DocRef) => void;
toggle: (doc: DocRef) => void;
close: () => void;
}
export const baseDocsUrl = 'https://docs.hatchet.run';
// Create a context for the docs state
export const DocsContext = createContext<DocsContextValue | null>(null);
// Hook to be used by consumers to access docs context
export function useDocs() {
const context = useContext(DocsContext);
if (!context) {
throw new Error('useDocs must be used within a DocsProvider');
}
return {
open: context.open,
toggle: context.toggle,
close: context.close,
sheet: context.sheet,
};
}
// Hook to create docs state (used by the provider only)
export function useDocsState(): DocsContextValue {
const [sheet, setSheet] = useState<DocsSheet>({
isOpen: false,
url: '',
title: '',
});
const openSheet = (doc: DocRef) => {
setSheet({
isOpen: true,
url: `${baseDocsUrl}${doc.href}`,
title: doc.title,
});
};
const closeSheet = () => {
setSheet((prev) => ({
...prev,
isOpen: false,
}));
};
const toggleSheet = (doc: DocRef) => {
if (sheet.isOpen) {
closeSheet();
} else {
openSheet(doc);
}
};
return {
sheet,
open: openSheet,
toggle: toggleSheet,
close: closeSheet,
};
}

View File

@@ -173,7 +173,11 @@ function RunsProviderContent({
? endOfMinute(new Date(timeRange.filters.endTime)).toISOString()
: endOfMinute(new Date()).toISOString();
const query = {
type WorkflowRunListParams = Parameters<
typeof api.v1WorkflowRunList
>[1];
const query: WorkflowRunListParams = {
offset: Math.max(
0,
(pagination.currentPage - 1) * pagination.pageSize,

View File

@@ -0,0 +1,131 @@
import {
createContext,
useCallback,
useContext,
useMemo,
useState,
} from 'react';
import docMetadata from '@/next/lib/docs';
import {
RunDetailSheet,
RunDetailSheetSerializableProps,
} from '../pages/authenticated/dashboard/runs/detail-sheet/run-detail-sheet';
export const pages = docMetadata;
export type DocRef = {
title: string;
href: string;
};
type SidePanelContent =
| {
isDocs: false;
component: React.ReactNode;
title: React.ReactNode;
actions?: React.ReactNode;
}
| {
isDocs: true;
title: string;
component: React.ReactNode;
};
type SidePanelData = {
content: SidePanelContent | null;
isOpen: boolean;
open: (props: UseSidePanelProps) => void;
close: () => void;
};
type UseSidePanelProps =
| {
type: 'docs';
content: DocRef;
}
| {
type: 'run-details';
content: RunDetailSheetSerializableProps;
};
export function useSidePanelData(): SidePanelData {
const [isOpen, setIsOpen] = useState(false);
const [props, setProps] = useState<UseSidePanelProps | null>(null);
const content = useMemo((): SidePanelContent | null => {
if (!props) {
return null;
}
const panelType = props.type;
switch (panelType) {
case 'run-details':
return {
isDocs: false,
component: <RunDetailSheet {...props.content} />,
title: 'Run Detail',
};
case 'docs':
return {
isDocs: true,
component: (
<div className="p-4 size-full">
<iframe
src={props.content.href}
className="inset-0 w-full rounded-md border border-slate-800 size-full"
title={`Documentation: ${props.content.title}`}
loading="lazy"
/>
</div>
),
title: props.content.title,
};
default:
// eslint-disable-next-line no-case-declarations
const exhaustiveCheck: never = panelType;
throw new Error(`Unhandled action type: ${exhaustiveCheck}`);
}
}, [props]);
const open = useCallback(
(props: UseSidePanelProps) => {
setProps(props);
setIsOpen(true);
},
[setIsOpen],
);
const close = useCallback(() => {
setIsOpen(false);
}, [setIsOpen]);
return {
isOpen,
content,
open,
close,
};
}
const SidePanelContext = createContext<SidePanelData | null>(null);
export function SidePanelProvider({ children }: { children: React.ReactNode }) {
const sidePanelState = useSidePanelData();
return (
<SidePanelContext.Provider value={sidePanelState}>
{children}
</SidePanelContext.Provider>
);
}
export function useSidePanel(): SidePanelData {
const context = useContext(SidePanelContext);
if (!context) {
throw new Error(
'useSidePanelContext must be used within a SidePanelProvider',
);
}
return context;
}

View File

@@ -1,161 +0,0 @@
import {
createContext,
useContext,
useState,
useMemo,
useCallback,
} from 'react';
import { RunDetailSheetSerializableProps } from '@/next/pages/authenticated/dashboard/runs/detail-sheet/run-detail-sheet';
import { useSearchParams } from 'react-router-dom';
import {
SHEET_PARAM_KEY,
encodeSheetProps,
decodeSheetProps,
} from '@/next/utils/sheet-url';
import { WorkerDetailsProps } from '../pages/authenticated/dashboard/workers/components/worker-details';
const EXPANDED_STATE_KEY = 'side-sheet-expanded-state';
export interface SideSheet {
isExpanded: boolean;
openProps?: OpenSheetProps;
}
export type OpenSheetProps =
| {
type: 'task-detail';
props: RunDetailSheetSerializableProps;
}
| {
type: 'worker-detail';
props: WorkerDetailsProps;
};
interface SideSheetContextValue {
sheet: SideSheet;
open: (props: OpenSheetProps) => void;
toggle: (props: OpenSheetProps) => void;
close: () => void;
toggleExpand: () => void;
}
// Create a context for the side sheet state
export const SideSheetContext = createContext<SideSheetContextValue | null>(
null,
);
// Hook to be used by consumers to access side sheet context
export function useSideSheet() {
const context = useContext(SideSheetContext);
if (!context) {
throw new Error('useSideSheet must be used within a SideSheetProvider');
}
return {
open: context.open,
toggle: context.toggle,
close: context.close,
sheet: context.sheet,
toggleExpand: context.toggleExpand,
};
}
// Hook to create side sheet state (used by the provider only)
export function useSideSheetState(): SideSheetContextValue {
const [searchParams, setSearchParams] = useSearchParams();
const [isExpanded, setIsExpanded] = useState<boolean>(() => {
try {
const savedExpandedState = localStorage.getItem(EXPANDED_STATE_KEY);
return savedExpandedState ? JSON.parse(savedExpandedState) : false;
} catch {
return false;
}
});
// Memoize decoded sheet properties to prevent unnecessary re-renders
const openProps = useMemo(() => {
const sheetParam = searchParams.get(SHEET_PARAM_KEY);
if (sheetParam) {
try {
return decodeSheetProps(sheetParam) as OpenSheetProps | undefined;
} catch {
return undefined;
}
}
return undefined;
}, [searchParams]);
// Memoize URL parameter update function
const updateUrlParams = useCallback(
(props?: OpenSheetProps) => {
const params = new URLSearchParams(searchParams.toString());
if (props) {
params.set(SHEET_PARAM_KEY, encodeSheetProps(props));
} else {
params.delete(SHEET_PARAM_KEY);
}
setSearchParams(params);
},
[searchParams, setSearchParams],
);
// Memoize sheet operations
const openSheet = useCallback(
(props: OpenSheetProps) => {
updateUrlParams(props);
},
[updateUrlParams],
);
const closeSheet = useCallback(() => {
updateUrlParams();
}, [updateUrlParams]);
const toggleSheet = useCallback(
(props: OpenSheetProps) => {
if (openProps) {
closeSheet();
} else {
openSheet(props);
}
},
[openProps, closeSheet, openSheet],
);
const toggleExpand = useCallback(() => {
const newExpandedState = !isExpanded;
try {
localStorage.setItem(
EXPANDED_STATE_KEY,
JSON.stringify(newExpandedState),
);
} catch {
// Ignore storage errors
}
setIsExpanded(newExpandedState);
}, [isExpanded]);
// Memoize sheet state
const sheet = useMemo<SideSheet>(
() => ({
isExpanded,
openProps,
}),
[isExpanded, openProps],
);
// Memoize context value
const contextValue = useMemo<SideSheetContextValue>(
() => ({
sheet,
open: openSheet,
toggle: toggleSheet,
close: closeSheet,
toggleExpand,
}),
[sheet, openSheet, toggleSheet, closeSheet, toggleExpand],
);
return contextValue;
}

View File

@@ -30,6 +30,7 @@ interface TaskRunDetailState {
>;
lastRefetchTime: number;
refetchInterval: number | undefined;
attempt?: number;
}
const TaskRunDetailContext = createContext<TaskRunDetailState | null>(null);
@@ -170,6 +171,7 @@ function TaskRunDetailProviderContent({
refetch: runDetails.refetch,
lastRefetchTime: lastRefetchTimeRef.current,
refetchInterval,
attempt,
}),
[
runDetails.data,
@@ -179,6 +181,7 @@ function TaskRunDetailProviderContent({
cancel,
replay,
refetchInterval,
attempt,
],
);

View File

@@ -47,24 +47,26 @@ export function FilterProvider<T extends Record<string, any>>({
}: FilterProviderProps<T>) {
const [filters, setFilters] = React.useState<T>(initialFilters);
const setFilter = React.useCallback(
(key: keyof T, value: T[keyof T]) => {
filters.setValue(key as string, value);
},
[filters],
);
const clearFilter = React.useCallback(
(key: keyof T) => {
filters.setValue(key as string, undefined as any);
},
[filters],
);
const setFilter = React.useCallback((key: keyof T, value: T[keyof T]) => {
setFilters((prev) => ({
...prev,
[key]: value,
}));
}, []);
const clearFilter = React.useCallback((key: keyof T) => {
setFilters((prev) => ({
...prev,
[key]: undefined as any,
}));
}, []);
const clearAllFilters = React.useCallback(() => {
const currentFilters = filters.getValues();
Object.keys(currentFilters).forEach((key) => {
filters.setValue(key, undefined as any);
});
const clearedFilters = Object.keys(filters).reduce((acc, key) => {
acc[key as keyof T] = undefined as any;
return acc;
}, {} as T);
setFilters(clearedFilters);
}, [filters]);
const value = React.useMemo(

View File

@@ -110,10 +110,17 @@ export function TimeFilterProvider({
children,
initialTimeRange,
}: TimeFilterProviderProps) {
const activePreset: TimeFilterState['activePreset'] =
initialTimeRange?.activePreset || '1h';
const lastActivePreset: TimeFilterState['activePreset'] =
initialTimeRange?.activePreset || '1h';
const [state, setState] = React.useState<TimeFilterState>({
activePreset: initialTimeRange?.activePreset || '1h',
lastActivePreset: initialTimeRange?.activePreset || '1h',
startTime: initialTimeRange?.startTime,
activePreset,
lastActivePreset,
startTime:
initialTimeRange?.startTime ||
TIME_PRESETS[activePreset](startOfMinute(new Date())).toISOString(),
endTime: initialTimeRange?.endTime,
});

View File

@@ -3,7 +3,7 @@ import { Snippet } from '@/next/lib/docs/generated/snips/types';
const snippet: Snippet = {
language: 'python',
content:
'from hatchet_sdk import Hatchet\n\nhatchet = Hatchet()\n\n# > Event trigger\nhatchet.event.push("user:create", {})\n',
'from hatchet_sdk import Hatchet\n\nhatchet = Hatchet()\n\n# > Event trigger\nhatchet.event.push("user:create", {"should_skip": False})\n',
source: 'out/python/events/event.py',
blocks: {
event_trigger: {

View File

@@ -1,6 +1,4 @@
import { V1WorkflowRun, WorkerType } from '@/lib/api';
import { SHEET_PARAM_KEY, encodeSheetProps } from '@/next/utils/sheet-url';
import { OpenSheetProps } from '../hooks/use-side-sheet';
// Base paths
export const BASE_PATH = '/next';
@@ -35,8 +33,6 @@ export const ROUTES = {
},
events: {
list: (tenantId: string) => `${FB.events(tenantId)}`,
detail: (tenantId: string, externalId: string) =>
`${FB.events(tenantId)}/${externalId}`,
},
learn: {
firstRun: (tenantId: string) => `${FB.learn(tenantId)}/first-run`,
@@ -45,20 +41,6 @@ export const ROUTES = {
list: (tenantId: string) => `${FB.runs(tenantId)}`,
detail: (tenantId: string, runId: string) =>
`${FB.runs(tenantId)}/${runId}`,
detailWithSheet: (
tenantId: string,
runId: string,
sheet: OpenSheetProps,
options?: { taskTab?: string },
) => {
const params = new URLSearchParams();
if (options?.taskTab) {
params.set('taskTab', options.taskTab);
}
params.set(SHEET_PARAM_KEY, encodeSheetProps(sheet));
return `${FB.runs(tenantId)}/${runId}?${params.toString()}`;
},
parent: (tenantId: string, run: V1WorkflowRun) =>
run.parentTaskExternalId
? `${FB.runs(tenantId)}/${run.parentTaskExternalId}`

View File

@@ -1,16 +1,14 @@
import { Link, useNavigate } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { UserLoginForm } from './components/user-login-form';
import { Button } from '@/components/ui/button';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import api, { UserLoginRequest } from '@/lib/api';
import { useState } from 'react';
import { useApiError } from '@/lib/hooks';
import { Loading } from '@/components/ui/loading';
import { Icons } from '@/components/ui/icons';
import React from 'react';
import React, { useState } from 'react';
import useApiMeta from '@/next/hooks/use-api-meta';
import useErrorParam from '@/pages/auth/hooks/use-error-param';
import { ROUTES } from '@/next/lib/routes';
import useUser from '@/next/hooks/use-user';
import { AxiosError } from 'axios';
export default function Login() {
useErrorParam();
@@ -20,7 +18,7 @@ export default function Login() {
return <Loading />;
}
const schemes = ['basic', 'google', 'github'];
const schemes = meta.oss?.auth?.schemes || [];
const basicEnabled = schemes.includes('basic');
const googleEnabled = schemes.includes('google');
const githubEnabled = schemes.includes('github');
@@ -42,7 +40,7 @@ export default function Login() {
basicEnabled && <BasicLogin />,
googleEnabled && <GoogleLogin />,
githubEnabled && <GithubLogin />,
].filter(Boolean);
].filter((x) => x !== undefined);
return (
<div className="flex flex-1 flex-col items-center justify-center w-full h-full lg:flex-row">
@@ -60,7 +58,9 @@ export default function Login() {
{forms.map((form, index) => (
<React.Fragment key={index}>
{form}
{index < schemes.length - 1 && <OrContinueWith />}
{basicEnabled && schemes.length >= 2 && index == 0 && (
<OrContinueWith />
)}
</React.Fragment>
))}
@@ -122,34 +122,21 @@ export function OrContinueWith() {
}
function BasicLogin() {
const navigate = useNavigate();
const { login } = useUser();
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
const { handleApiError } = useApiError({ setFieldErrors });
const queryClient = useQueryClient();
const loginMutation = useMutation({
mutationKey: ['user:update:login'],
mutationFn: async (data: UserLoginRequest) => {
return api.userUpdateLogin(data);
},
onSuccess: async () => {
await queryClient.invalidateQueries();
const memberships = await api.tenantMembershipsList();
const tenant = memberships?.data?.rows?.at(0)?.tenant?.metadata.id;
if (tenant) {
navigate(ROUTES.runs.list(tenant));
} else {
navigate('/next');
}
},
onError: handleApiError,
});
return (
<UserLoginForm
isLoading={loginMutation.isPending}
onSubmit={loginMutation.mutate}
isLoading={login.isPending}
onSubmit={async (data) => {
try {
await login.mutateAsync(data);
window.location.href = '/';
} catch (error) {
if (error instanceof AxiosError) {
setFieldErrors(error.response?.data.errors || {});
}
}
}}
fieldErrors={fieldErrors}
/>
);

View File

@@ -11,19 +11,16 @@ import {
import useUser from '@/next/hooks/use-user';
import { ROUTES } from '@/next/lib/routes';
import { useTenantDetails } from '@/next/hooks/use-tenant';
import { Loading } from '@/components/ui/loading';
export default function Register() {
const { oss: meta, isLoading } = useApiMeta();
const { oss, isLoading } = useApiMeta();
if (isLoading) {
return 'Loading...'; // TODO: add loading
return <Loading />;
}
if (!meta) {
return 'Error loading meta'; // TODO: add error
}
const schemes = meta.auth?.schemes || [];
const schemes = oss?.auth?.schemes || [];
const basicEnabled = schemes.includes('basic');
const googleEnabled = schemes.includes('google');
const githubEnabled = schemes.includes('github');
@@ -52,7 +49,9 @@ export default function Register() {
{forms.map((form, index) => (
<React.Fragment key={index}>
{form}
{index < schemes.length - 1 && <OrContinueWith />}
{basicEnabled && schemes.length >= 2 && index == 0 && (
<OrContinueWith />
)}
</React.Fragment>
))}
<div className="flex flex-col space-y-2">

View File

@@ -1,7 +1,6 @@
'use client';
import {
BookOpen,
CheckIcon,
ChevronRight,
ChevronsUpDown,
@@ -52,7 +51,6 @@ import { useCurrentTenantId, useTenantDetails } from '@/next/hooks/use-tenant';
import { useNavigate, useLocation, Link } from 'react-router-dom';
import { Logo } from '@/next/components/ui/logo';
import { Code } from '@/next/components/ui/code';
import { pages, useDocs } from '@/next/hooks/use-docs-sheet';
import { ROUTES } from '@/next/lib/routes';
import {
DropdownMenu,
@@ -63,6 +61,8 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/next/components/ui/dropdown-menu';
import { DocsButton } from '@/next/components/ui/docs-button';
import docMetadata from '@/next/lib/docs';
export function AppSidebar({ children }: PropsWithChildren) {
const meta = useApiMeta();
@@ -74,7 +74,7 @@ export function AppSidebar({ children }: PropsWithChildren) {
const location = useLocation();
const navLinks = getMainNavLinks(tenantId, location.pathname);
const { toggleSidebar, isCollapsed, isMobile, setOpenMobile } = useSidebar();
const docs = useDocs();
const [collapsibleState, setCollapsibleState] = useState<
Record<string, boolean>
>({});
@@ -294,14 +294,13 @@ name: ${user?.name}`;
</DropdownMenu>
</SidebarMenuItem>
<SidebarMenuItem key="docs">
<SidebarMenuButton
size="sm"
tooltip="Documentation"
onClick={() => docs.toggle(pages.home.index)}
>
<BookOpen />
<span>Documentation</span>
</SidebarMenuButton>
<DocsButton
doc={docMetadata.home.index}
prefix={''}
titleOverride="Documentation"
variant="ghost"
className="px-2 hover:bg-background"
/>
</SidebarMenuItem>
{navLinks.navSecondary.map((item) => (
<SidebarMenuItem key={item.title}>

View File

@@ -1,81 +0,0 @@
import { RunsTable } from '@/next/components/runs/runs-table/runs-table';
import { DocsButton } from '@/next/components/ui/docs-button';
import {
Headline,
HeadlineActionItem,
HeadlineActions,
PageTitle,
} from '@/next/components/ui/page-header';
import { Separator } from '@/next/components/ui/separator';
import docs from '@/next/lib/docs';
import { RunsProvider } from '@/next/hooks/use-runs';
import { useMemo } from 'react';
import { V1TaskSummary } from '@/lib/api';
import { TimeFilters } from '@/next/components/ui/filters/time-filter-group';
import { RunsMetricsView } from '@/next/components/runs/runs-metrics/runs-metrics';
import { useParams } from 'react-router-dom';
import BasicLayout from '@/next/components/layouts/basic.layout';
import { useSideSheet } from '@/next/hooks/use-side-sheet';
export default function EventsDetailPage() {
const { eventId } = useParams<{
eventId: string;
}>();
const { open: openSideSheet, sheet } = useSideSheet();
const handleRowClick = (task: V1TaskSummary) => {
openSideSheet({
type: 'task-detail',
props: {
selectedWorkflowRunId: task.workflowRunExternalId,
selectedTaskId: task.taskExternalId,
pageWorkflowRunId: '',
},
});
};
const selectedTaskId = useMemo(() => {
if (sheet?.openProps?.type === 'task-detail') {
return sheet?.openProps?.props.selectedTaskId;
}
return undefined;
}, [sheet]);
return (
<BasicLayout>
<Headline>
<PageTitle description={`Viewing runs triggered by event ${eventId}`}>
Runs
</PageTitle>
<HeadlineActions>
<HeadlineActionItem>
<DocsButton doc={docs.home.run_on_event} size="icon" />
</HeadlineActionItem>
</HeadlineActions>
</Headline>
<Separator className="my-4" />
<RunsProvider
initialFilters={{ triggering_event_external_id: eventId }}
initialTimeRange={{
activePreset: '7d',
}}
>
<div className="flex flex-col gap-4 mt-4">
<TimeFilters />
<RunsMetricsView />
<RunsTable
onRowClick={handleRowClick}
selectedTaskId={selectedTaskId}
excludedFilters={[
'additional_metadata',
'is_root_task',
'statuses',
'workflow_ids',
]}
/>
</div>
</RunsProvider>
</BasicLayout>
);
}

View File

@@ -1,8 +1,8 @@
import { V1Event } from '@/lib/api';
import { V1Event, V1TaskStatus } from '@/lib/api';
import BasicLayout from '@/next/components/layouts/basic.layout';
import { RunsBadge } from '@/next/components/runs/runs-badge';
import { DataTableColumnHeader } from '@/next/components/runs/runs-table/data-table-column-header';
import { Badge } from '@/next/components/ui/badge';
import { Button } from '@/next/components/ui/button';
import { RunsTable } from '@/next/components/runs/runs-table/runs-table';
import { DataTable } from '@/next/components/ui/data-table';
import { DocsButton } from '@/next/components/ui/docs-button';
import {
@@ -18,17 +18,19 @@ import {
} from '@/next/components/ui/pagination';
import RelativeDate from '@/next/components/ui/relative-date';
import { Separator } from '@/next/components/ui/separator';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/next/components/ui/tooltip';
import { EventsProvider, useEvents } from '@/next/hooks/use-events';
import { useCurrentTenantId } from '@/next/hooks/use-tenant';
import { RunsProvider } from '@/next/hooks/use-runs';
import docs from '@/next/lib/docs';
import { ROUTES } from '@/next/lib/routes';
import { AdditionalMetadata } from '@/pages/main/v1/events/components/additional-metadata';
import { ColumnDef } from '@tanstack/react-table';
import { Link } from 'react-router-dom';
function EventsContent() {
const { data, isLoading } = useEvents();
const { tenantId } = useCurrentTenantId();
if (isLoading) {
return (
@@ -38,13 +40,6 @@ function EventsContent() {
);
}
// const eventKeys = Array.from(new Set(data.map((e) => e.key)))
// .sort((a, b) => a.localeCompare(b))
// .map((k) => ({
// label: k,
// value: k,
// }));
return (
<BasicLayout>
<Headline>
@@ -58,17 +53,8 @@ function EventsContent() {
</HeadlineActions>
</Headline>
<Separator className="my-4" />
{/* <FilterGroup>
<div className="flex flex-row gap-x-4">
<FilterSelect<EventsFilters, string>
name="keys"
placeholder="Event Key"
/>
<ClearFiltersButton />
</div>
</FilterGroup> */}
<DataTable
columns={columns(tenantId)}
columns={columns()}
data={data || []}
emptyState={
<div className="flex flex-col items-center justify-center gap-4 py-8">
@@ -85,7 +71,7 @@ function EventsContent() {
);
}
export const columns = (tenantId: string): ColumnDef<V1Event>[] => {
export const columns = (): ColumnDef<V1Event>[] => {
return [
{
accessorKey: 'EventId',
@@ -93,11 +79,7 @@ export const columns = (tenantId: string): ColumnDef<V1Event>[] => {
<DataTableColumnHeader column={column} title="ID" className="pl-4" />
),
cell: ({ row }) => (
<div className="w-full">
<Link to={ROUTES.events.detail(tenantId, row.original.metadata.id)}>
<Button variant="link">{row.original.metadata.id}</Button>
</Link>
</div>
<div className="w-full">{row.original.metadata.id} </div>
),
enableSorting: false,
enableHiding: true,
@@ -145,20 +127,32 @@ export const columns = (tenantId: string): ColumnDef<V1Event>[] => {
};
return (
<div className="flex flex-row gap-2 items-center justify-start">
{!!queued && <Badge variant="outline">{queued} Queued</Badge>}
{!!running && (
<Badge className="bg-amber-400">{running} Running</Badge>
)}
{!!cancelled && (
<Badge className="bg-black border border-red-500 text-white">
{cancelled} Cancelled
</Badge>
)}
{!!succeeded && (
<Badge variant="successful">{succeeded} Succeeded</Badge>
)}
{!!failed && <Badge variant="destructive">{failed} Failed</Badge>}
<div className="flex flex-row gap-2 items-center justify-start w-max">
<StatusBadgeWithTooltip
count={queued}
eventExternalId={row.original.metadata.id}
status={V1TaskStatus.QUEUED}
/>
<StatusBadgeWithTooltip
count={running}
eventExternalId={row.original.metadata.id}
status={V1TaskStatus.RUNNING}
/>
<StatusBadgeWithTooltip
count={cancelled}
eventExternalId={row.original.metadata.id}
status={V1TaskStatus.CANCELLED}
/>
<StatusBadgeWithTooltip
count={succeeded}
eventExternalId={row.original.metadata.id}
status={V1TaskStatus.COMPLETED}
/>
<StatusBadgeWithTooltip
count={failed}
eventExternalId={row.original.metadata.id}
status={V1TaskStatus.FAILED}
/>
</div>
);
},
@@ -208,3 +202,50 @@ export default function EventsPage() {
</EventsProvider>
);
}
const StatusBadgeWithTooltip = ({
count,
eventExternalId,
status,
}: {
count: number | undefined;
eventExternalId: string;
status: V1TaskStatus;
}) => {
if (!count || count === 0) {
return null;
}
return (
<Tooltip>
<TooltipTrigger asChild>
<div>
<RunsBadge status={status} variant="default" />
</div>
</TooltipTrigger>
<TooltipContent className="bg-[hsl(var(--background))] border-slate-700 border z-20 shadow-lg p-4 text-white">
<RunsProvider
initialFilters={{
triggering_event_external_id: eventExternalId,
}}
>
<RunsTable
excludedFilters={[
'additional_metadata',
'only_tasks',
'is_root_task',
'parent_task_external_id',
'statuses',
'triggering_event_external_id',
'worker_id',
'workflow_ids',
]}
showPagination={false}
allowSelection={false}
showActions={false}
/>
</RunsProvider>
</TooltipContent>
</Tooltip>
);
};

View File

@@ -10,13 +10,4 @@ export const eventsRoutes = [
};
}),
},
{
path: ROUTES.events.detail(':tenantId', ':eventId'),
lazy: () =>
import('./events-detail.page').then((res) => {
return {
Component: res.default,
};
}),
},
];

View File

@@ -1,23 +0,0 @@
import { RunDataCard } from '@/next/components/runs/run-output-card';
import {
V1TaskSummary,
V1WorkflowRunDetails,
} from '@/lib/api/generated/data-contracts';
export type RunDetailRawContentProps = {
selectedTask?: V1TaskSummary | V1WorkflowRunDetails | null;
};
export const RunDetailRawContent = ({
selectedTask,
}: RunDetailRawContentProps) => {
if (!selectedTask) {
return null;
}
return (
<>
<RunDataCard title="Raw" output={selectedTask} variant="input" />
</>
);
};

View File

@@ -7,7 +7,6 @@ import {
import { RunDetailProvider, useRunDetail } from '@/next/hooks/use-run-detail';
import { TaskRunDetailPayloadContent } from './task-run-detail-payloads';
import { RunEventLog } from '@/next/components/runs/run-event-log/run-event-log';
import { useSideSheet } from '@/next/hooks/use-side-sheet';
import { useMemo } from 'react';
import { Button } from '@/next/components/ui/button';
import { AlertCircle, ArrowUpCircle } from 'lucide-react';
@@ -27,8 +26,9 @@ import {
TaskRunDetailProvider,
useTaskRunDetail,
} from '@/next/hooks/use-task-run-detail';
import { RunDetailRawContent } from './run-detail-raw';
import { WorkflowRunDetailPayloadContent } from './workflow-run-detail-payloads';
import { RunDataCard } from '@/next/components/runs/run-output-card';
import { useSidePanel } from '@/next/hooks/use-side-panel';
export interface RunDetailSheetSerializableProps {
pageWorkflowRunId?: string;
selectedWorkflowRunId: string;
@@ -37,12 +37,7 @@ export interface RunDetailSheetSerializableProps {
attempt?: number;
}
interface RunDetailSheetProps extends RunDetailSheetSerializableProps {
isOpen: boolean;
onClose: () => void;
}
export function RunDetailSheet(props: RunDetailSheetProps) {
export function RunDetailSheet(props: RunDetailSheetSerializableProps) {
return (
<RunDetailProvider
runId={props.selectedWorkflowRunId}
@@ -61,11 +56,10 @@ export function RunDetailSheet(props: RunDetailSheetProps) {
function RunDetailSheetContent() {
const { data } = useRunDetail();
const { data: selectedTask } = useTaskRunDetail();
const { open: openSheet, sheet } = useSideSheet();
const { data: selectedTask, attempt } = useTaskRunDetail();
const { open: openSheet } = useSidePanel();
const { selectedTaskId, attempt } = sheet.openProps
?.props as RunDetailSheetSerializableProps;
const selectedTaskId = data?.run.metadata.id;
const latestTask = useMemo(() => {
return data?.tasks.find((task) => task.metadata.id === selectedTaskId);
@@ -78,272 +72,289 @@ function RunDetailSheetContent() {
const isDAG = data?.shape.length && data?.shape.length > 1;
return (
<>
<div className="h-full flex flex-col relative">
<div className="sticky top-0 z-10 bg-slate-100 dark:bg-slate-900 px-4 pb-2">
<div className="flex flex-row items-center justify-between">
<div className="flex flex-col gap-2 mt-4">
<div className="flex flex-row items-center gap-2">
<RunsBadge status={data?.run?.status} variant="xs" />
<RunId
wfRun={data?.run}
onClick={() => {
if (!data?.run?.metadata.id) {
return;
}
openSheet({
type: 'task-detail',
props: {
selectedWorkflowRunId: data.run.metadata.id,
},
});
}}
/>
{isDAG && selectedTask && <>/</>}
</div>
{isDAG && selectedTask && (
<div className="flex flex-row items-center gap-2">
<RunsBadge status={selectedTask?.status} variant="xs" />
<RunId wfRun={selectedTask} onClick={() => {}} />
</div>
)}
</div>
<div className="flex items-center gap-2">
{isDAG ? (
<>
<Badge variant="outline">DAG</Badge>
</>
) : (
<Badge variant="outline">Standalone</Badge>
)}
</div>
</div>
</div>
<div className="flex-1 overflow-y-clip">
<div className="bg-slate-100 dark:bg-slate-900">
<WorkflowRunVisualizer
workflowRunId={data?.run?.metadata.id || ''}
patchTask={selectedTask}
onTaskSelect={(taskId, childWorkflowRunId) => {
<div className="flex-1 flex flex-col min-h-0">
{/* Header with run id, run type, etc. */}
<div className="bg-slate-100 dark:bg-slate-900 px-4 pb-2 flex flex-row items-center justify-between ">
<div className="flex flex-col gap-2 pt-4">
<div className="flex flex-row items-center gap-2">
<RunsBadge status={data?.run?.status} variant="xs" />
<RunId
wfRun={data?.run}
onClick={() => {
if (!data?.run?.metadata.id) {
return;
}
openSheet({
type: 'task-detail',
props: {
selectedWorkflowRunId:
childWorkflowRunId || data?.run?.metadata.id,
selectedTaskId: taskId,
attempt: undefined,
type: 'run-details',
content: {
selectedWorkflowRunId: data.run.metadata.id,
},
});
}}
selectedTaskId={
isDAG && selectedTask?.taskExternalId === data?.run?.metadata.id
? undefined
: selectedTask?.taskExternalId
}
/>
{isDAG && selectedTask && <>/</>}
</div>
{latestTask?.attempt && populatedAttempt && (
<div className="sticky top-[72px] z-10 bg-slate-100 dark:bg-slate-900 px-4 py-2 flex items-center justify-between text-sm">
<div
className={cn(
'flex items-center gap-1.5 text-yellow-700',
populatedAttempt === latestTask.attempt && 'text-green-700',
)}
>
{populatedAttempt !== latestTask.attempt && (
<>
<AlertCircle className="h-3.5 w-3.5" />{' '}
<span>
Viewing attempt {populatedAttempt} of {latestTask.attempt}
</span>
</>
)}
</div>
<div className="flex flex-row items-center gap-2">
{latestTask.attempt > populatedAttempt && (
<Button
variant="link"
size="sm"
tooltip="View latest attempt"
disabled={populatedAttempt === latestTask.attempt}
onClick={() => {
if (
!data?.run?.metadata.id ||
!selectedTask?.taskExternalId
) {
return;
}
openSheet({
type: 'task-detail',
props: {
selectedWorkflowRunId: data.run.metadata.id,
selectedTaskId: selectedTask.taskExternalId,
attempt: latestTask.attempt,
},
});
}}
>
<ArrowUpCircle className="h-4 w-4" />
</Button>
)}
{selectedTask && (
<Select
value={populatedAttempt?.toString() || '0'}
onValueChange={(value) => {
if (
!data?.run?.metadata.id ||
!selectedTask?.taskExternalId
) {
return;
}
openSheet({
type: 'task-detail',
props: {
selectedWorkflowRunId: data.run.metadata.id,
selectedTaskId: selectedTask.taskExternalId,
attempt: parseInt(value),
},
});
}}
>
<SelectTrigger className="h-6 text-xs">
<SelectValue placeholder="Attempt" />
</SelectTrigger>
<SelectContent>
{Array.from(
{ length: latestTask?.attempt || 0 },
(_, i) => i,
)
.reverse()
.map((i) => (
<SelectItem key={i} value={(i + 1).toString()}>
Attempt {i + 1}{' '}
{i + 1 == latestTask.attempt ? ' (Current)' : ''}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
{isDAG && selectedTask && (
<div className="flex flex-row items-center gap-2">
<RunsBadge status={selectedTask?.status} variant="xs" />
<RunId wfRun={selectedTask} onClick={() => {}} />
</div>
)}
<Tabs defaultValue="payload" className="w-full">
<TabsList
layout="underlined"
className="w-full sticky top-0 z-10 bg-slate-100 dark:bg-slate-900"
>
<TabsTrigger variant="underlined" value="payload">
Payloads
</TabsTrigger>
<TabsTrigger variant="underlined" value="activity">
Activity
</TabsTrigger>
{selectedTask && (
<TabsTrigger variant="underlined" value="worker">
Worker
</TabsTrigger>
</div>
<div className="flex items-center gap-2">
{isDAG ? (
<>
<Badge variant="outline">DAG</Badge>
</>
) : (
<Badge variant="outline">Standalone</Badge>
)}
</div>
</div>
{/* Content */}
<div className="overflow-y-auto flex flex-col h-full min-h-0">
<div className="bg-slate-100 dark:bg-slate-900">
<WorkflowRunVisualizer
workflowRunId={data?.run?.metadata.id || ''}
patchTask={selectedTask}
onTaskSelect={(taskId, childWorkflowRunId) => {
if (!data?.run?.metadata.id) {
return;
}
openSheet({
type: 'run-details',
content: {
selectedWorkflowRunId:
childWorkflowRunId || data?.run?.metadata.id,
selectedTaskId: taskId,
attempt: undefined,
},
});
}}
selectedTaskId={
isDAG && selectedTask?.taskExternalId === data?.run?.metadata.id
? undefined
: selectedTask?.taskExternalId
}
/>
</div>
{latestTask?.attempt && populatedAttempt && (
<div className="z-10 bg-slate-100 dark:bg-slate-900 px-4 py-2 flex items-center justify-between text-sm">
<div
className={cn(
'flex items-center gap-1.5 text-yellow-700',
populatedAttempt === latestTask.attempt && 'text-green-700',
)}
<TabsTrigger variant="underlined" value="raw">
Raw
</TabsTrigger>
</TabsList>
<TabsContent value="activity" className="mt-4">
{data?.run && (
<RunEventLog
workflow={data.run}
showNextButton={
selectedTask &&
populatedAttempt &&
populatedAttempt < (latestTask?.attempt || 0)
? {
label: `Next attempt (${populatedAttempt + 1} of ${latestTask?.attempt})`,
onClick: () => {
if (
!data?.run?.metadata.id ||
!selectedTask?.taskExternalId
) {
return;
}
openSheet({
type: 'task-detail',
props: {
selectedWorkflowRunId: data.run.metadata.id,
selectedTaskId: selectedTask.taskExternalId,
attempt: populatedAttempt + 1,
},
});
},
}
: undefined
}
showPreviousButton={
selectedTask && populatedAttempt && populatedAttempt > 1
? {
label: `Previous attempt (${populatedAttempt - 1} of ${latestTask?.attempt})`,
onClick: () => {
if (
!data?.run?.metadata.id ||
!selectedTask?.taskExternalId
) {
return;
}
openSheet({
type: 'task-detail',
props: {
selectedWorkflowRunId: data.run.metadata.id,
selectedTaskId: selectedTask.taskExternalId,
attempt: populatedAttempt - 1,
},
});
},
}
: undefined
}
filters={{
taskId: selectedTaskId ? [selectedTaskId] : undefined,
attempt: populatedAttempt,
}}
showFilters={{
taskId: false,
attempt: false,
}}
onTaskSelect={(event) => {
>
{populatedAttempt !== latestTask.attempt && (
<>
<AlertCircle className="h-3.5 w-3.5" />{' '}
<span>
Viewing attempt {populatedAttempt} of {latestTask.attempt}
</span>
</>
)}
</div>
<div className="flex flex-row items-center gap-2">
{latestTask.attempt > populatedAttempt && (
<Button
variant="link"
size="sm"
tooltip="View latest attempt"
disabled={populatedAttempt === latestTask.attempt}
onClick={() => {
if (
!data?.run?.metadata.id ||
!selectedTask?.taskExternalId
) {
return;
}
openSheet({
type: 'task-detail',
props: {
type: 'run-details',
content: {
selectedWorkflowRunId: data.run.metadata.id,
selectedTaskId: event.taskId,
attempt: event.attempt,
selectedTaskId: selectedTask.taskExternalId,
attempt: latestTask.attempt,
},
});
}}
/>
>
<ArrowUpCircle className="h-4 w-4" />
</Button>
)}
</TabsContent>
<TabsContent value="payload" className="mt-4">
<div className="flex flex-col gap-4">
{selectedTask ? (
<TaskRunDetailPayloadContent
selectedTask={selectedTask}
attempt={populatedAttempt}
{selectedTask && (
<Select
value={populatedAttempt?.toString() || '0'}
onValueChange={(value) => {
if (
!data?.run?.metadata.id ||
!selectedTask?.taskExternalId
) {
return;
}
openSheet({
type: 'run-details',
content: {
selectedWorkflowRunId: data.run.metadata.id,
selectedTaskId: selectedTask.taskExternalId,
attempt: parseInt(value),
},
});
}}
>
<SelectTrigger className="h-6 text-xs">
<SelectValue placeholder="Attempt" />
</SelectTrigger>
<SelectContent>
{Array.from(
{ length: latestTask?.attempt || 0 },
(_, i) => i,
)
.reverse()
.map((i) => (
<SelectItem key={i} value={(i + 1).toString()}>
Attempt {i + 1}{' '}
{i + 1 == latestTask.attempt ? ' (Current)' : ''}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
</div>
)}
<Tabs
defaultValue="payload"
className="w-full flex-1 flex flex-col min-h-0"
>
<TabsList
layout="underlined"
className="w-full z-10 bg-slate-100 dark:bg-slate-900 flex-shrink-0"
>
<TabsTrigger variant="underlined" value="payload">
Payloads
</TabsTrigger>
<TabsTrigger variant="underlined" value="activity">
Activity
</TabsTrigger>
{selectedTask && (
<TabsTrigger variant="underlined" value="worker">
Worker
</TabsTrigger>
)}
<TabsTrigger variant="underlined" value="raw">
Raw
</TabsTrigger>
</TabsList>
<TabsContent value="activity" className="flex-1 min-h-0">
<div className="h-full overflow-y-auto px-4">
<div className="pt-4 pb-4">
{data?.run && (
<RunEventLog
workflow={data.run}
showNextButton={
selectedTask &&
populatedAttempt &&
populatedAttempt < (latestTask?.attempt || 0)
? {
label: `Next attempt (${populatedAttempt + 1} of ${latestTask?.attempt})`,
onClick: () => {
if (
!data?.run?.metadata.id ||
!selectedTask?.taskExternalId
) {
return;
}
openSheet({
type: 'run-details',
content: {
selectedWorkflowRunId: data.run.metadata.id,
selectedTaskId: selectedTask.taskExternalId,
attempt: populatedAttempt + 1,
},
});
},
}
: undefined
}
showPreviousButton={
selectedTask && populatedAttempt && populatedAttempt > 1
? {
label: `Previous attempt (${populatedAttempt - 1} of ${latestTask?.attempt})`,
onClick: () => {
if (
!data?.run?.metadata.id ||
!selectedTask?.taskExternalId
) {
return;
}
openSheet({
type: 'run-details',
content: {
selectedWorkflowRunId: data.run.metadata.id,
selectedTaskId: selectedTask.taskExternalId,
attempt: populatedAttempt - 1,
},
});
},
}
: undefined
}
filters={{
taskId: selectedTaskId ? [selectedTaskId] : undefined,
attempt: populatedAttempt,
}}
showFilters={{
taskId: false,
attempt: false,
}}
onTaskSelect={(event) => {
openSheet({
type: 'run-details',
content: {
selectedWorkflowRunId: data.run.metadata.id,
selectedTaskId: event.taskId,
attempt: event.attempt,
},
});
}}
/>
) : (
<WorkflowRunDetailPayloadContent workflowRun={data?.run} />
)}
</div>
</TabsContent>
<TabsContent value="worker" className="mt-4">
{/* TODO: Add worker details */}
</TabsContent>
<TabsContent value="raw" className="mt-4">
<RunDetailRawContent selectedTask={selectedTask || data} />
</TabsContent>
</Tabs>
</div>
</div>
</TabsContent>
<TabsContent
value="payload"
className="flex-1 flex-col gap-4 min-h-0 h-full overflow-y-auto p-4 pb-4"
>
<div className="flex flex-col gap-4">
{selectedTask ? (
<TaskRunDetailPayloadContent
selectedTask={selectedTask}
attempt={populatedAttempt}
/>
) : (
<WorkflowRunDetailPayloadContent workflowRun={data?.run} />
)}
</div>
</TabsContent>
<TabsContent value="worker" className="flex-1 min-h-0 pb-4">
<div className="text-center text-gray-500">
Worker details coming soon
</div>
</TabsContent>
<TabsContent
value="raw"
className="flex-1 min-h-0 pb-4 h-full overflow-y-auto px-4 "
>
<RunDataCard
title="Raw"
output={selectedTask || data}
variant="input"
/>
</TabsContent>
</Tabs>
</div>
</>
</div>
);
}

View File

@@ -38,7 +38,7 @@ import RelativeDate from '@/next/components/ui/relative-date';
import { WorkflowDetailsProvider } from '@/next/hooks/use-workflow-details';
import WorkflowGeneralSettings from '../workflows/settings';
import BasicLayout from '@/next/components/layouts/basic.layout';
import { useSideSheet } from '@/next/hooks/use-side-sheet';
import { useSidePanel } from '@/next/hooks/use-side-panel';
export default function RunDetailPage() {
const { workflowRunId } = useParams<{
@@ -64,17 +64,19 @@ function RunDetailPageContent({ workflowRunId }: RunDetailPageProps) {
const { data, isLoading, error, cancel, replay } = useRunDetail();
const [showTriggerModal, setShowTriggerModal] = useState(false);
const [selectedTaskId, setSelectedTaskId] = useState<string>();
const workflow = data?.run;
const tasks = data?.tasks;
const { open: openSheet, sheet } = useSideSheet();
const { open: openSheet } = useSidePanel();
const handleTaskSelect = useCallback(
(taskId: string, childWorkflowRunId?: string) => {
setSelectedTaskId(taskId);
openSheet({
type: 'task-detail',
props: {
type: 'run-details',
content: {
pageWorkflowRunId: workflowRunId!,
selectedWorkflowRunId: childWorkflowRunId || taskId,
selectedTaskId: taskId,
@@ -84,13 +86,6 @@ function RunDetailPageContent({ workflowRunId }: RunDetailPageProps) {
[openSheet, workflowRunId],
);
const selectedTaskId = useMemo(() => {
if (sheet?.openProps?.type === 'task-detail') {
return sheet?.openProps?.props.selectedTaskId;
}
return undefined;
}, [sheet]);
const canCancel = useMemo(() => {
return (
tasks &&
@@ -317,8 +312,8 @@ function RunDetailPageContent({ workflowRunId }: RunDetailPageProps) {
workflow={workflow}
onTaskSelect={(event) => {
openSheet({
type: 'task-detail',
props: {
type: 'run-details',
content: {
selectedWorkflowRunId: workflowRunId!,
selectedTaskId: event.taskId,
attempt: event.attempt,

View File

@@ -13,22 +13,24 @@ import { Separator } from '@/next/components/ui/separator';
import docs from '@/next/lib/docs';
import { RunsProvider } from '@/next/hooks/use-runs';
import { Plus } from 'lucide-react';
import { useMemo, useState } from 'react';
import { useState } from 'react';
import { V1TaskSummary } from '@/lib/api';
import { TimeFilters } from '@/next/components/ui/filters/time-filter-group';
import { RunsMetricsView } from '@/next/components/runs/runs-metrics/runs-metrics';
import BasicLayout from '@/next/components/layouts/basic.layout';
import { useSideSheet } from '@/next/hooks/use-side-sheet';
import { useSidePanel } from '@/next/hooks/use-side-panel';
export default function RunsPage() {
const [showTriggerModal, setShowTriggerModal] = useState(false);
const [selectedTaskId, setSelectedTaskId] = useState<string>();
const { open: openSideSheet, sheet } = useSideSheet();
const { open: openSidePanel } = useSidePanel();
const handleRowClick = (task: V1TaskSummary) => {
openSideSheet({
type: 'task-detail',
props: {
setSelectedTaskId(task.taskExternalId);
openSidePanel({
type: 'run-details',
content: {
selectedWorkflowRunId: task.workflowRunExternalId,
selectedTaskId: task.taskExternalId,
pageWorkflowRunId: '',
@@ -36,13 +38,6 @@ export default function RunsPage() {
});
};
const selectedTaskId = useMemo(() => {
if (sheet?.openProps?.type === 'task-detail') {
return sheet?.openProps?.props.selectedTaskId;
}
return undefined;
}, [sheet]);
return (
<BasicLayout>
<Headline>
@@ -71,6 +66,7 @@ export default function RunsPage() {
onRowClick={handleRowClick}
selectedTaskId={selectedTaskId}
onTriggerRunClick={() => setShowTriggerModal(true)}
excludedFilters={['worker_id']}
/>
</div>
<TriggerRunModal

View File

@@ -3,7 +3,7 @@ import { Headline, PageTitle } from '@/next/components/ui/page-header';
import docs from '@/next/lib/docs';
import BasicLayout from '@/next/components/layouts/basic.layout';
import { Separator } from '@/next/components/ui/separator';
import { baseDocsUrl } from '@/next/hooks/use-docs-sheet';
import { baseDocsUrl } from '@/next/components/ui/docs-button';
function WorkerPoolDetailPageContent() {
return (

View File

@@ -1,4 +1,3 @@
import { useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { WorkersProvider } from '@/next/hooks/use-workers';
import { Separator } from '@/next/components/ui/separator';
@@ -15,26 +14,30 @@ import BasicLayout from '@/next/components/layouts/basic.layout';
import { RunsProvider } from '@/next/hooks/use-runs';
import { RunsTable } from '@/next/components/runs/runs-table/runs-table';
import { V1TaskSummary } from '@/lib/api';
import { useSideSheet } from '@/next/hooks/use-side-sheet';
import { RunsMetricsView } from '@/next/components/runs/runs-metrics/runs-metrics';
import { TimeFilters } from '@/next/components/ui/filters/time-filter-group';
import { useWorker } from '@/next/hooks/use-worker';
import { Spinner } from '@/next/components/ui/spinner';
import { WorkerActions } from './components/actions';
import { useSidePanel } from '@/next/hooks/use-side-panel';
import { useState } from 'react';
function WorkerDetailPageContent() {
const [selectedTaskId, setSelectedTaskId] = useState<string>();
const { workerId } = useParams();
const { data: workerDetails, isLoading: isWorkerLoading } = useWorker({
workerId: workerId || '',
});
const { open: openSideSheet, sheet } = useSideSheet();
const { open: openSideSheet } = useSidePanel();
const handleRowClick = (task: V1TaskSummary) => {
setSelectedTaskId(task.taskExternalId);
openSideSheet({
type: 'task-detail',
props: {
type: 'run-details',
content: {
selectedWorkflowRunId: task.workflowRunExternalId,
selectedTaskId: task.taskExternalId,
pageWorkflowRunId: '',
@@ -42,13 +45,6 @@ function WorkerDetailPageContent() {
});
};
const selectedTaskId = useMemo(() => {
if (sheet?.openProps?.type === 'task-detail') {
return sheet?.openProps?.props.selectedTaskId;
}
return undefined;
}, [sheet]);
if (isWorkerLoading) {
return <Spinner />;
}
@@ -57,12 +53,10 @@ function WorkerDetailPageContent() {
return <div>Worker not found</div>;
}
const description = `Viewing tasks executed by worker ${workerId}`;
return (
<BasicLayout>
<Headline>
<PageTitle description={description}>
<PageTitle description={`Viewing tasks executed by worker ${workerId}`}>
{workerDetails.name} <Badge variant="outline">Self-hosted</Badge>
</PageTitle>
<HeadlineActions>

View File

@@ -2,40 +2,63 @@ import RelativeDate from '@/components/v1/molecules/relative-date';
import { Workflow } from '@/lib/api';
import { Badge } from '@/next/components/ui/badge';
import { Button } from '@/next/components/ui/button';
import { Skeleton } from '@/next/components/ui/skeleton';
import { useCurrentTenantId } from '@/next/hooks/use-tenant';
import { WorkflowsProvider, useWorkflows } from '@/next/hooks/use-workflows';
import { ROUTES } from '@/next/lib/routes';
import { ArrowPathIcon } from '@heroicons/react/24/outline';
import { useState } from 'react';
import { Link } from 'react-router-dom';
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardFooter,
} from '@/next/components/ui/card';
import { DocsButton } from '@/next/components/ui/docs-button';
import docs from '@/next/lib/docs';
import { HeadlineActionItem } from '@/next/components/ui/page-header';
import { HeadlineActions } from '@/next/components/ui/page-header';
import { PageTitle } from '@/next/components/ui/page-header';
import BasicLayout from '@/next/components/layouts/basic.layout';
import { Headline } from '@/next/components/ui/page-header';
const WorkflowCard: React.FC<{ data: Workflow }> = ({ data }) => (
<div
key={data.metadata?.id}
className="border overflow-hidden shadow rounded-lg"
>
<div className="px-4 py-5 sm:p-6">
<div className="flex flex-row justify-between items-center">
<h3 className="text-lg leading-6 font-medium text-foreground">
<Link to={`/next/workflows/${data.metadata?.id}`}>{data.name}</Link>
</h3>
{data.isPaused ? (
<Badge variant="default">Paused</Badge> // TODO: This should be `inProgress`
) : (
<Badge variant="outline">Active</Badge> // TODO: This should be `successful`
)}
const WorkflowCard: React.FC<{ data: Workflow }> = ({ data }) => {
const { tenantId } = useCurrentTenantId();
return (
<div
key={data.metadata?.id}
className="border overflow-hidden shadow rounded-lg"
>
<div className="px-4 py-5 sm:p-6">
<div className="flex flex-row justify-between items-center">
<h3 className="text-lg leading-6 font-medium text-foreground">
<Link to={ROUTES.workflows.detail(tenantId, data.metadata.id)}>
{data.name}
</Link>
</h3>
{data.isPaused ? (
<Badge variant="default">Paused</Badge> // TODO: This should be `inProgress`
) : (
<Badge variant="outline">Active</Badge> // TODO: This should be `successful`
)}
</div>
<p className="mt-1 max-w-2xl text-sm text-gray-700 dark:text-gray-300">
Created at <RelativeDate date={data.metadata?.createdAt} />
</p>
</div>
<p className="mt-1 max-w-2xl text-sm text-gray-700 dark:text-gray-300">
Created at <RelativeDate date={data.metadata?.createdAt} />
</p>
</div>
<div className="px-4 py-4 sm:px-6">
<div className="text-sm text-background-secondary">
<Link to={`/next/workflows/${data.metadata?.id}`}>
<Button>View Workflow</Button>
</Link>
<div className="px-4 py-4 sm:px-6">
<div className="text-sm text-background-secondary">
<Link to={ROUTES.workflows.detail(tenantId, data.metadata.id)}>
<Button>View Workflow</Button>
</Link>
</div>
</div>
</div>
</div>
);
);
};
function WorkflowsContent() {
const { data, isLoading, invalidate } = useWorkflows();
@@ -43,44 +66,80 @@ function WorkflowsContent() {
if (isLoading) {
return (
<>
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
<div className="aspect-video rounded-xl bg-muted/50" />
<div className="aspect-video rounded-xl bg-muted/50" />
<div className="aspect-video rounded-xl bg-muted/50" />
</div>
<div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min" />
<div className="flex flex-1 flex-col gap-4 p-4 pt-16">
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
{Array.from({ length: 9 }).map((_, ix) => (
<Skeleton key={ix} className="h-40 rounded-md" />
))}
</div>
</>
</div>
);
}
const emptyState = (
<Card className="w-full text-justify">
<CardHeader>
<CardTitle>No Tasks or Workflows Found</CardTitle>
<CardDescription>
<p className="text-gray-700 dark:text-gray-300 mb-4">
There are no tasks or workflows registered in this tenant, please
register a task or workflow with a worker.
</p>
</CardDescription>
</CardHeader>
<CardFooter className="flex flex-col gap-2">
<DocsButton
variant="default"
doc={docs.home.your_first_task}
titleOverride="declaring tasks"
/>
<DocsButton doc={docs.home.workers} titleOverride="registering tasks" />
</CardFooter>
</Card>
);
return (
<div className="flex flex-col gap-4 p-4">
<div className="flex flex-row items-end justify-end w-full">
<Button
key="refresh"
className="h-8 px-2 lg:px-3"
size="sm"
onClick={async () => {
invalidate();
setRotate(!rotate);
}}
variant={'outline'}
aria-label="Refresh events list"
>
<ArrowPathIcon
className={`h-4 w-4 transition-transform ${rotate ? 'rotate-180' : ''}`}
/>
</Button>
</div>
<div className="grid grid-cols-3 gap-2">
{data.map((workflow) => (
<WorkflowCard key={workflow.metadata?.id} data={workflow} />
))}
</div>
</div>
<BasicLayout>
<Headline>
<PageTitle description="View and manage workload that is registered on this tenant">
Tasks and Workflows
</PageTitle>
<HeadlineActions>
<HeadlineActionItem>
<DocsButton doc={docs.home.your_first_task} size="icon" />
</HeadlineActionItem>
<HeadlineActionItem>
<Button
key="refresh"
className="h-8 px-2 lg:px-3"
size="sm"
onClick={async () => {
invalidate();
setRotate(!rotate);
}}
variant={'outline'}
aria-label="Refresh workflows list"
>
<ArrowPathIcon
className={`h-4 w-4 transition-transform ${rotate ? 'rotate-180' : ''}`}
/>
</Button>
</HeadlineActionItem>
</HeadlineActions>
</Headline>
{!data || data.length === 0 ? (
emptyState
) : (
<div className="flex flex-col gap-4">
<div className="grid grid-cols-3 gap-2">
{data.map((workflow) => (
<WorkflowCard key={workflow.metadata?.id} data={workflow} />
))}
</div>
</div>
)}
</BasicLayout>
);
}

View File

@@ -2,50 +2,172 @@ import { CenterStageLayout } from '@/next/components/layouts/center-stage.layout
import { ThemeProvider } from '@/next/components/theme-provider';
import { ApiConnectionError } from '@/next/components/errors/api-connection-error';
import useApiMeta from '@/next/hooks/use-api-meta';
import { PropsWithChildren } from 'react';
import {
PropsWithChildren,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { Outlet } from 'react-router-dom';
import { DocsSheetComponent } from '@/next/components/ui/docs-sheet';
import { Toaster } from '@/next/components/ui/toaster';
import { ToastProvider } from '@/next/hooks/utils/use-toast';
import { SideSheetComponent } from '../components/ui/sheet/side-sheet.layout';
import {
SideSheetContext,
useSideSheetState,
} from '@/next/hooks/use-side-sheet';
import { SidePanel } from '../components/ui/sheet/side-sheet.layout';
import { SidebarProvider } from '@/next/components/ui/sidebar';
import { DocsContext, useDocsState } from '../hooks/use-docs-sheet';
import { SidePanelProvider, useSidePanel } from '../hooks/use-side-panel';
function usePersistentPanelWidth(defaultWidth: number = 67) {
const [leftPanelWidth, setLeftPanelWidth] = useState(() => {
try {
const savedWidth = localStorage.getItem('leftPanelWidth');
return savedWidth ? parseFloat(savedWidth) : defaultWidth;
} catch {
return defaultWidth;
}
});
const updateLeftPanelWidth = useCallback((width: number) => {
const clampedWidth = Math.min(Math.max(width, 20), 80);
setLeftPanelWidth(clampedWidth);
try {
localStorage.setItem('leftPanelWidth', clampedWidth.toString());
} catch {
// Silently fail if localStorage is not available
}
}, []);
return [leftPanelWidth, updateLeftPanelWidth] as const;
}
function RootContent({ children }: PropsWithChildren) {
const meta = useApiMeta();
const docsState = useDocsState();
const sideSheetState = useSideSheetState();
const [leftPanelWidth, setLeftPanelWidth] = usePersistentPanelWidth(67);
const [isDragging, setIsDragging] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const { isOpen: isRightPanelOpen } = useSidePanel();
const handleMouseDown = useCallback(
(e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
if (!isRightPanelOpen) {
return;
}
setIsDragging(true);
e.preventDefault();
},
[isRightPanelOpen],
);
const handleMouseMove = useCallback(
(e: MouseEvent) => {
if (!isDragging || !containerRef.current) {
return;
}
const containerRect = containerRef.current.getBoundingClientRect();
const newLeftWidth =
((e.clientX - containerRect.left) / containerRect.width) * 100;
setLeftPanelWidth(newLeftWidth);
},
[isDragging, setLeftPanelWidth],
);
const handleMouseUp = useCallback(() => {
setIsDragging(false);
}, []);
useEffect(() => {
if (isDragging) {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}
}, [isDragging, handleMouseMove, handleMouseUp]);
useEffect(() => {
if (isRightPanelOpen && leftPanelWidth === 100) {
try {
const savedWidth = localStorage.getItem('leftPanelWidth');
if (savedWidth) {
setLeftPanelWidth(parseFloat(savedWidth));
}
} catch {
setLeftPanelWidth(67);
}
}
}, [isRightPanelOpen, leftPanelWidth, setLeftPanelWidth]);
return (
<SideSheetContext.Provider value={sideSheetState}>
<DocsContext.Provider value={docsState}>
<div className="flex h-screen w-full">
{meta.isLoading ? (
<CenterStageLayout>
<div className="flex h-screen w-full items-center justify-center"></div>
</CenterStageLayout>
) : meta.hasFailed ? (
<ApiConnectionError
retryInterval={meta.refetchInterval}
errorMessage={meta.hasFailed.message}
/>
) : (
<>
{children ?? <Outlet />}
<SideSheetComponent />
<DocsSheetComponent
sheet={docsState.sheet}
onClose={docsState.close}
<>
{meta.isLoading ? (
<CenterStageLayout>
<div className="flex h-screen w-full items-center justify-center"></div>
</CenterStageLayout>
) : meta.hasFailed ? (
<ApiConnectionError
retryInterval={meta.refetchInterval}
errorMessage={meta.hasFailed.message}
/>
) : (
<div
ref={containerRef}
className="h-screen flex flex-1 relative overflow-hidden"
>
{/* Left panel - main content */}
<div
className="h-full overflow-auto flex-shrink-0"
style={{
width: isRightPanelOpen ? `${leftPanelWidth}%` : '100%',
}}
>
{children ?? <Outlet />}
</div>
{/* Resize handle */}
{isRightPanelOpen && (
<div
className="relative w-1 flex-shrink-0 group cursor-col-resize"
onMouseDown={handleMouseDown}
>
<div className="absolute inset-0 -left-2 -right-2 w-5" />
<div
className={`
h-full w-full transition-colors duration-150
${
isDragging
? 'bg-blue-500'
: 'bg-border hover:bg-blue-400/50 group-hover:bg-blue-400/50'
}
`}
/>
</>
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-150">
<div className="flex flex-col space-y-1">
<div className="w-0.5 h-0.5 bg-white/60 rounded-full" />
<div className="w-0.5 h-0.5 bg-white/60 rounded-full" />
<div className="w-0.5 h-0.5 bg-white/60 rounded-full" />
</div>
</div>
</div>
)}
{/* Right panel - side sheet */}
{isRightPanelOpen && (
<div className="flex flex-col overflow-hidden flex-1">
<SidePanel />
</div>
)}
</div>
</DocsContext.Provider>
</SideSheetContext.Provider>
)}
</>
);
}
@@ -54,8 +176,10 @@ function Root({ children }: PropsWithChildren) {
<ToastProvider>
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
<SidebarProvider>
<Toaster />
<RootContent>{children}</RootContent>
<SidePanelProvider>
<Toaster />
<RootContent>{children}</RootContent>
</SidePanelProvider>
</SidebarProvider>
</ThemeProvider>
</ToastProvider>

View File

@@ -18,7 +18,7 @@ import {
} from 'react-router-dom';
import { Tenant, TenantMember, TenantUIVersion } from '@/lib/api';
import { ClockIcon, GearIcon } from '@radix-ui/react-icons';
import React, { useCallback } from 'react';
import React, { useCallback, useEffect } from 'react';
import {
MembershipsContextType,
UserContextType,
@@ -40,7 +40,6 @@ import { ROUTES } from '@/next/lib/routes';
function Main() {
const ctx = useOutletContext<UserContextType & MembershipsContextType>();
const navigate = useNavigate();
const { user, memberships } = ctx;
const { tenant: currTenant } = useTenant();
@@ -51,30 +50,21 @@ function Main() {
tenant: currTenant,
});
useEffect(() => {
if (!currTenant) {
return;
}
if (currTenant.uiVersion === TenantUIVersion.V1) {
// Hard redirect here because the navigate hook is racy with other url updates
window.location.href = ROUTES.runs.list(currTenant.metadata.id);
}
}, [currTenant, navigate]);
if (!user || !memberships || !currTenant) {
return <Loading />;
}
if (currTenant.uiVersion === TenantUIVersion.V1) {
// FIXME: Not sure why `<Navigate />` is not working here
// think it's probably because of an effect hook race condition
// where the URL is updated in two places
return (
<div className="flex flex-col w-full items-center mt-4">
<div className="flex flex-col items-center justify-center border rounded-lg p-4 max-w-96 gap-y-6">
You're currently on a V0 UI page, but you've upgraded to the V1 UI.
<Button
onClick={() => {
navigate(ROUTES.runs.list(currTenant.metadata.id));
}}
>
Take me to the V1 UI
</Button>
</div>
</div>
);
}
return (
<div className="flex flex-row flex-1 w-full h-full">
<Sidebar memberships={memberships} currTenant={currTenant} />

View File

@@ -2,7 +2,7 @@ import { Snippet } from '@/lib/generated/snips/types';
const snippet: Snippet = {
"language": "python",
"content": "from hatchet_sdk import Hatchet\n\nhatchet = Hatchet()\n\n# > Event trigger\nhatchet.event.push(\"user:create\", {})\n",
"content": "from hatchet_sdk import Hatchet\n\nhatchet = Hatchet()\n\n# > Event trigger\nhatchet.event.push(\"user:create\", {\"should_skip\": False})\n",
"source": "out/python/events/event.py",
"blocks": {
"event_trigger": {

View File

@@ -3,5 +3,5 @@ from hatchet_sdk import Hatchet
hatchet = Hatchet()
# > Event trigger
hatchet.event.push("user:create", {})
hatchet.event.push("user:create", {"should_skip": False})
# !!