From 453199b2ab265dc84c0d842d2ea79cd81486fe8a Mon Sep 17 00:00:00 2001 From: KarlaCarvajal Date: Tue, 10 Dec 2024 09:00:28 -0600 Subject: [PATCH 1/4] fix(docs): replace java comment by python one in LH doc (#1186) --- .../05-developer-guide/08-wfspec-development/08-user-tasks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/05-developer-guide/08-wfspec-development/08-user-tasks.md b/docs/docs/05-developer-guide/08-wfspec-development/08-user-tasks.md index e378bc251..8d544b691 100644 --- a/docs/docs/05-developer-guide/08-wfspec-development/08-user-tasks.md +++ b/docs/docs/05-developer-guide/08-wfspec-development/08-user-tasks.md @@ -233,7 +233,7 @@ func QuickstartWorkflow(wf *littlehorse.WorkflowThread) { def my_entrypoint(wf: WorkflowThread) -> None: task_def_name = "greet" user_task_output = wf.assign_user_task("person-details", None, "writer-group") - delay_in_seconds = 10 // wait 10 seconds after the task is assigned to schedule the reminder + delay_in_seconds = 10 # wait 10 seconds after the task is assigned to schedule the reminder arg1 = "Sam" arg2 = {"identification": "1258796641-4", "Address": "NA-Street", "Age": 28} From 7bd73022ccfd8e9bf531876f46215d1951586c53 Mon Sep 17 00:00:00 2001 From: "Bryson G." <114206517+bryson-g@users.noreply.github.com> Date: Tue, 10 Dec 2024 18:03:37 -0800 Subject: [PATCH 2/4] feat(dashboard): user task audit log (#1183) --- dashboard/package-lock.json | 2 +- dashboard/package.json | 2 +- .../components/ExternalLinkButton.tsx | 19 ++++ .../(diagram)/components/NodeRunsList.tsx | 9 +- .../NodeTypes/UserTask/UserTask.tsx | 29 +++--- .../[...props]/components/WfRunsHeader.tsx | 4 +- .../src/app/[tenantId]/components/Details.tsx | 37 ++++++++ .../[tenantId]/components/LinkWithTenant.tsx | 11 ++- .../[...props]/components/Details.tsx | 13 ++- .../[...props]/components/UserTaskDef.tsx | 12 ++- .../[...props]/components/Versions.tsx | 1 + .../userTaskDef/audit/[...ids]/AuditTable.tsx | 88 +++++++++++++++++++ .../userTaskDef/audit/[...ids]/page.tsx | 54 ++++++++++++ dashboard/src/app/actions/getUserTaskRun.tsx | 9 ++ dashboard/src/components/ui/button.tsx | 2 +- dashboard/src/components/ui/pagination.tsx | 81 +++++++++++++++++ 16 files changed, 350 insertions(+), 23 deletions(-) create mode 100644 dashboard/src/app/[tenantId]/(diagram)/components/ExternalLinkButton.tsx create mode 100644 dashboard/src/app/[tenantId]/components/Details.tsx create mode 100644 dashboard/src/app/[tenantId]/userTaskDef/audit/[...ids]/AuditTable.tsx create mode 100644 dashboard/src/app/[tenantId]/userTaskDef/audit/[...ids]/page.tsx create mode 100644 dashboard/src/app/actions/getUserTaskRun.tsx create mode 100644 dashboard/src/components/ui/pagination.tsx diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index bd7a0733d..9ed39d2d5 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -16,7 +16,7 @@ "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.1.0", - "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.1", "@tanstack/react-query": "^5.37.1", "class-variance-authority": "^0.7.0", diff --git a/dashboard/package.json b/dashboard/package.json index 61d32d65e..8ccf848b5 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -19,7 +19,7 @@ "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.1.0", - "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.1", "@tanstack/react-query": "^5.37.1", "class-variance-authority": "^0.7.0", diff --git a/dashboard/src/app/[tenantId]/(diagram)/components/ExternalLinkButton.tsx b/dashboard/src/app/[tenantId]/(diagram)/components/ExternalLinkButton.tsx new file mode 100644 index 000000000..e9070ebd9 --- /dev/null +++ b/dashboard/src/app/[tenantId]/(diagram)/components/ExternalLinkButton.tsx @@ -0,0 +1,19 @@ +import { Button } from '@/components/ui/button' +import LinkWithTenant from '@/app/[tenantId]/components/LinkWithTenant' +import { ExternalLinkIcon } from 'lucide-react' +import { ComponentProps } from 'react' +import { cn } from '@/components/utils' + +type ExternalLinkButtonProps = { + href: string + label: string + target?: string +} & ComponentProps + +export const ExternalLinkButton = ({ href, label, target, className, ...props }: ExternalLinkButtonProps) => ( + +) diff --git a/dashboard/src/app/[tenantId]/(diagram)/components/NodeRunsList.tsx b/dashboard/src/app/[tenantId]/(diagram)/components/NodeRunsList.tsx index e4ec2102b..484a9c95b 100644 --- a/dashboard/src/app/[tenantId]/(diagram)/components/NodeRunsList.tsx +++ b/dashboard/src/app/[tenantId]/(diagram)/components/NodeRunsList.tsx @@ -2,6 +2,7 @@ import { NodeRun, TaskNode, UserTaskNode } from 'littlehorse-client/proto' import { EyeIcon } from 'lucide-react' import { FC, useCallback } from 'react' import { useModal } from '../hooks/useModal' +import { Button } from '@/components/ui/button' type Prop = { nodeRuns: [NodeRun] @@ -20,10 +21,14 @@ export const NodeRunsList: FC = ({ nodeRuns, taskNode, userTaskNode, nodeR return (
- +
) } diff --git a/dashboard/src/app/[tenantId]/(diagram)/components/NodeTypes/UserTask/UserTask.tsx b/dashboard/src/app/[tenantId]/(diagram)/components/NodeTypes/UserTask/UserTask.tsx index 4f502cdc4..6570206e6 100644 --- a/dashboard/src/app/[tenantId]/(diagram)/components/NodeTypes/UserTask/UserTask.tsx +++ b/dashboard/src/app/[tenantId]/(diagram)/components/NodeTypes/UserTask/UserTask.tsx @@ -1,31 +1,36 @@ import { UserTaskDefDetails } from '@/app/[tenantId]/(diagram)/components/NodeTypes/UserTask/UserTaskDefDetails' import LinkWithTenant from '@/app/[tenantId]/components/LinkWithTenant' -import { ExternalLinkIcon, UserIcon } from 'lucide-react' +import { ExternalLinkIcon, EyeIcon, UserIcon } from 'lucide-react' import { FC, memo } from 'react' import { Handle, Position } from 'reactflow' import { NodeRunsList } from '../../NodeRunsList' import { Fade } from '../Fade' import { NodeProps } from '../index' import { NodeDetails } from '../NodeDetails' +import { useParams, useRouter } from 'next/navigation' +import { Button } from '@/components/ui/button' +import { ExternalLinkButton } from '../../ExternalLinkButton' const Node: FC = ({ data, selected }) => { + const router = useRouter() + const tenantId = useParams().tenantId as string + if (!data.userTask) return null const { fade, userTask, nodeRun, nodeNeedsToBeHighlighted, nodeRunsList } = data return ( <> -
-
-

UserTask

- - {userTask.userTaskDefName} - -
+
+

UserTask

+ + {nodeRun && ( + + )} + {nodeRun ? ( ) : ( diff --git a/dashboard/src/app/[tenantId]/(diagram)/wfSpec/[...props]/components/WfRunsHeader.tsx b/dashboard/src/app/[tenantId]/(diagram)/wfSpec/[...props]/components/WfRunsHeader.tsx index 8347737fa..465f2e752 100644 --- a/dashboard/src/app/[tenantId]/(diagram)/wfSpec/[...props]/components/WfRunsHeader.tsx +++ b/dashboard/src/app/[tenantId]/(diagram)/wfSpec/[...props]/components/WfRunsHeader.tsx @@ -27,7 +27,9 @@ export const WfRunsHeader: FC = ({ spec, currentStatus, currentWindow, se
- {currentWindow !== -1 ? `Last ${TIME_RANGES_NAMES[currentWindow]}` : TIME_RANGES_NAMES[currentWindow]} + + {currentWindow !== -1 ? `Last ${TIME_RANGES_NAMES[currentWindow]}` : TIME_RANGES_NAMES[currentWindow]} +
diff --git a/dashboard/src/app/[tenantId]/components/Details.tsx b/dashboard/src/app/[tenantId]/components/Details.tsx new file mode 100644 index 000000000..26874a1ca --- /dev/null +++ b/dashboard/src/app/[tenantId]/components/Details.tsx @@ -0,0 +1,37 @@ +import { TagIcon } from 'lucide-react' +import { FC } from 'react' +import { Versions } from '../userTaskDef/[...props]/components/Versions' + +export type DetailsProps = { + itemHeader: string + header: string + version?: number[] + status?: string + description?: Record +} + +export const Details: FC = ({ itemHeader, header, version, status, description }) => { + return ( +
+
+ {itemHeader} +

{header}

+
+ {version && version.length > 1 ? ( +
+ + {version[0]} +
+ ) : null} +
+ {description && + Object.entries(description).map(([key, value]) => ( +
+ {key}: {value} +
+ ))} + {status &&
{status}
} +
+
+ ) +} diff --git a/dashboard/src/app/[tenantId]/components/LinkWithTenant.tsx b/dashboard/src/app/[tenantId]/components/LinkWithTenant.tsx index 441ee144d..f0b0ca947 100644 --- a/dashboard/src/app/[tenantId]/components/LinkWithTenant.tsx +++ b/dashboard/src/app/[tenantId]/components/LinkWithTenant.tsx @@ -1,11 +1,18 @@ 'use client' +import { cn } from '@/components/utils' import NextLink from 'next/link' import { useParams } from 'next/navigation' import { ComponentProps } from 'react' -const LinkWithTenant = ({ ...props }: ComponentProps) => { +const LinkWithTenant = ({ linkStyle, ...props }: ComponentProps & { linkStyle?: boolean }) => { const { tenantId } = useParams() - return + return ( + + ) } export default LinkWithTenant diff --git a/dashboard/src/app/[tenantId]/userTaskDef/[...props]/components/Details.tsx b/dashboard/src/app/[tenantId]/userTaskDef/[...props]/components/Details.tsx index 4c3882bc9..12ed9fd5d 100644 --- a/dashboard/src/app/[tenantId]/userTaskDef/[...props]/components/Details.tsx +++ b/dashboard/src/app/[tenantId]/userTaskDef/[...props]/components/Details.tsx @@ -2,18 +2,27 @@ import { UserTaskDef } from 'littlehorse-client/proto' import { FC } from 'react' import { Versions } from './Versions' +import { TagIcon } from 'lucide-react' type Props = { id: Pick + staticVersion?: boolean } -export const Details: FC = ({ id }) => { +export const Details: FC = ({ id, staticVersion = false }) => { return (
UserTaskDef

{id.name}

{id.description &&
{id.description}
}
- + {staticVersion ? ( +
+ + {id.version} +
+ ) : ( + + )}
) diff --git a/dashboard/src/app/[tenantId]/userTaskDef/[...props]/components/UserTaskDef.tsx b/dashboard/src/app/[tenantId]/userTaskDef/[...props]/components/UserTaskDef.tsx index ade44c9e7..8288689ba 100644 --- a/dashboard/src/app/[tenantId]/userTaskDef/[...props]/components/UserTaskDef.tsx +++ b/dashboard/src/app/[tenantId]/userTaskDef/[...props]/components/UserTaskDef.tsx @@ -21,7 +21,7 @@ import React, { FC, Fragment, useState } from 'react' import { useDebounce } from 'use-debounce' import { Details } from './Details' import { Fields } from './Fields' -import { useParams } from 'next/navigation' +import { useParams, useRouter } from 'next/navigation' type Props = { spec: UserTaskDefProto @@ -43,6 +43,7 @@ export const UserTaskDef: FC = ({ spec }) => { const [limit, setLimit] = useState(SEARCH_DEFAULT_LIMIT) const [userIdToSearchFor] = useDebounce(userId, DEBOUNCE_DELAY) const [userGroupToSearchFor] = useDebounce(userGroup, DEBOUNCE_DELAY) + const router = useRouter() const { isPending, data, hasNextPage, fetchNextPage } = useInfiniteQuery({ queryKey: [ @@ -178,6 +179,15 @@ export const UserTaskDef: FC = ({ spec }) => { ? utcToLocalDateTime(userTaskRun.scheduledTime) : NOT_APPLICABLE_LABEL} + + + ) }) diff --git a/dashboard/src/app/[tenantId]/userTaskDef/[...props]/components/Versions.tsx b/dashboard/src/app/[tenantId]/userTaskDef/[...props]/components/Versions.tsx index b1417df83..ce5e68a61 100644 --- a/dashboard/src/app/[tenantId]/userTaskDef/[...props]/components/Versions.tsx +++ b/dashboard/src/app/[tenantId]/userTaskDef/[...props]/components/Versions.tsx @@ -1,3 +1,4 @@ +'use client' import { VersionSelector } from '@/app/[tenantId]/components/VersionSelector' import { UserTaskDefId } from 'littlehorse-client/proto' import { useParams } from 'next/navigation' diff --git a/dashboard/src/app/[tenantId]/userTaskDef/audit/[...ids]/AuditTable.tsx b/dashboard/src/app/[tenantId]/userTaskDef/audit/[...ids]/AuditTable.tsx new file mode 100644 index 000000000..8ba55b54e --- /dev/null +++ b/dashboard/src/app/[tenantId]/userTaskDef/audit/[...ids]/AuditTable.tsx @@ -0,0 +1,88 @@ +'use client' +import { useState } from 'react' +import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' +import { + Pagination, + PaginationContent, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, +} from '@/components/ui/pagination' +import { UserTaskEvent } from 'littlehorse-client/proto' + +type AuditTableProps = { + events: UserTaskEvent[] +} + +export function AuditTable({ events }: AuditTableProps) { + const [currentPage, setCurrentPage] = useState(1) + const itemsPerPage = 10 + + const allEvents = events.filter(event => event.saved !== undefined).reverse() + + const totalPages = Math.ceil(allEvents.length / itemsPerPage) + const startIndex = (currentPage - 1) * itemsPerPage + const paginatedEvents = allEvents.slice(startIndex, startIndex + itemsPerPage) + + return ( +
+ + Audit Log + + + Saved At + Saved By + + + + {paginatedEvents.map(event => ( + + {new Date(event.time ?? 'N/A').toLocaleString()} + {event.saved?.userId} + + ))} + +
+ + {totalPages > 1 && ( + + + + { + e.preventDefault() + if (currentPage > 1) setCurrentPage(currentPage - 1) + }} + /> + + {[...Array(totalPages)].map((_, index) => ( + + { + e.preventDefault() + setCurrentPage(index + 1) + }} + > + {index + 1} + + + ))} + + { + e.preventDefault() + if (currentPage < totalPages) setCurrentPage(currentPage + 1) + }} + /> + + + + )} +
+ ) +} diff --git a/dashboard/src/app/[tenantId]/userTaskDef/audit/[...ids]/page.tsx b/dashboard/src/app/[tenantId]/userTaskDef/audit/[...ids]/page.tsx new file mode 100644 index 000000000..f3877a604 --- /dev/null +++ b/dashboard/src/app/[tenantId]/userTaskDef/audit/[...ids]/page.tsx @@ -0,0 +1,54 @@ +import { getUserTaskRun } from '@/app/actions/getUserTaskRun' +import { Metadata } from 'next' +import { notFound } from 'next/navigation' +import { ClientError, Status } from 'nice-grpc-common' +import { Details } from '@/app/[tenantId]/components/Details' +import LinkWithTenant from '@/app/[tenantId]/components/LinkWithTenant' +import { AuditTable } from './AuditTable' +import Link from 'next/link' +import { Button } from '@/components/ui/button' + +type Props = { params: { ids: string[]; tenantId: string } } + +export default async function Page({ params: { ids, tenantId } }: Props) { + const [wfRunId, userTaskGuid] = ids + + try { + const userTaskRun = await getUserTaskRun(tenantId, wfRunId, userTaskGuid) + + return ( +
+
+ {wfRunId} + + ), + userTaskRunId: userTaskGuid, + }} + /> + {userTaskRun.events.some(event => event.saved !== undefined) ? ( + + ) : ( +
+

No save history found.

+

This UserTaskRun has not been saved since it was created.

+
+ )} +
+ ) + } catch (error) { + if (error instanceof ClientError && error.code === Status.NOT_FOUND) return notFound() + throw error + } +} + +export async function generateMetadata({ params: { ids } }: Props): Promise { + return { + title: `UserTask Audit ${ids[1]} | Littlehorse`, + } +} diff --git a/dashboard/src/app/actions/getUserTaskRun.tsx b/dashboard/src/app/actions/getUserTaskRun.tsx new file mode 100644 index 000000000..a48bbbda6 --- /dev/null +++ b/dashboard/src/app/actions/getUserTaskRun.tsx @@ -0,0 +1,9 @@ +'use server' + +import { WfRunId } from 'littlehorse-client/proto' +import { lhClient } from '../lhClient' + +export async function getUserTaskRun(tenantId: string, wfRunId: string, userTaskGuid: string) { + const client = await lhClient({ tenantId }) + return await client.getUserTaskRun({ wfRunId: { id: wfRunId }, userTaskGuid }) +} diff --git a/dashboard/src/components/ui/button.tsx b/dashboard/src/components/ui/button.tsx index 139ecd222..131f160e2 100644 --- a/dashboard/src/components/ui/button.tsx +++ b/dashboard/src/components/ui/button.tsx @@ -14,7 +14,7 @@ const buttonVariants = cva( outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', ghost: 'hover:bg-accent hover:text-accent-foreground', - link: 'text-primary underline-offset-4 hover:underline', + link: 'text-primary underline-offset-4 hover:underline text-blue-500 h-0', }, size: { default: 'h-10 px-4 py-2', diff --git a/dashboard/src/components/ui/pagination.tsx b/dashboard/src/components/ui/pagination.tsx new file mode 100644 index 000000000..74addbf4d --- /dev/null +++ b/dashboard/src/components/ui/pagination.tsx @@ -0,0 +1,81 @@ +import * as React from 'react' +import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react' + +import { cn } from '@/components/utils' +import { ButtonProps, buttonVariants } from '@/components/ui/button' + +const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => ( +