mirror of
https://github.com/hatchet-dev/hatchet.git
synced 2026-02-08 17:18:41 -06:00
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:
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -3,7 +3,7 @@ on:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- "sdks/**"
|
||||
- "frontend/docs/**"
|
||||
- "frontend/**"
|
||||
- "examples/**"
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
131
frontend/app/src/next/hooks/use-side-panel.tsx
Normal file
131
frontend/app/src/next/hooks/use-side-panel.tsx
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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}`
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,13 +10,4 @@ export const eventsRoutes = [
|
||||
};
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: ROUTES.events.detail(':tenantId', ':eventId'),
|
||||
lazy: () =>
|
||||
import('./events-detail.page').then((res) => {
|
||||
return {
|
||||
Component: res.default,
|
||||
};
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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})
|
||||
# !!
|
||||
|
||||
Reference in New Issue
Block a user