diff --git a/.gitignore b/.gitignore index dfd1ec023..98c5d40eb 100644 --- a/.gitignore +++ b/.gitignore @@ -46,4 +46,6 @@ dist/ # dotnet global.json -*DotSettings.user \ No newline at end of file +*DotSettings.user +!sdk-dotnet/Littlehorse.sln +*.sln \ No newline at end of file diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index 60895852c..bd7a0733d 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -9,10 +9,10 @@ "version": "0.1.0", "hasInstallScript": true, "dependencies": { - "@headlessui/react": "^2.0.3", "@radix-ui/react-accordion": "^1.2.1", "@radix-ui/react-checkbox": "^1.1.1", "@radix-ui/react-dialog": "^1.1.2", + "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.1.0", @@ -1135,21 +1135,6 @@ "@floating-ui/utils": "^0.2.8" } }, - "node_modules/@floating-ui/react": { - "version": "0.26.28", - "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", - "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.1.2", - "@floating-ui/utils": "^0.2.8", - "tabbable": "^6.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, "node_modules/@floating-ui/react-dom": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", @@ -1169,25 +1154,6 @@ "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==", "license": "MIT" }, - "node_modules/@headlessui/react": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.0.tgz", - "integrity": "sha512-RzCEg+LXsuI7mHiSomsu/gBJSjpupm6A1qIZ5sWjd7JhARNlMiSA4kKfJpCKwU9tE+zMRterhhrP74PvfJrpXQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/react": "^0.26.16", - "@react-aria/focus": "^3.17.1", - "@react-aria/interactions": "^3.21.3", - "@tanstack/react-virtual": "^3.8.1" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "react": "^18 || ^19 || ^19.0.0-rc", - "react-dom": "^18 || ^19 || ^19.0.0-rc" - } - }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -3151,6 +3117,35 @@ } } }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.2.tgz", + "integrity": "sha512-GVZMR+eqK8/Kes0a36Qrv+i20bAPXSn8rCBTHx30w+3ECnR5o3xixAlqcVaYvLeyKUsm0aqyhWfmUcqufM8nYA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-menu": "2.1.2", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-focus-guards": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", @@ -3232,6 +3227,46 @@ } } }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.2.tgz", + "integrity": "sha512-lZ0R4qR2Al6fZ4yCCZzu/ReTFrylHFxIqy7OezIpWF4bL0o9biKo0pFIvkaew3TyZ9Fy5gYVrR5zCGZBVbO1zg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-collection": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.1", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.0", + "@radix-ui/react-portal": "1.1.2", + "@radix-ui/react-presence": "1.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-roving-focus": "1.1.0", + "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.6.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz", @@ -3350,6 +3385,52 @@ } } }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz", + "integrity": "sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-collection": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-context": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", + "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-select": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.2.tgz", @@ -3609,134 +3690,6 @@ "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==", "license": "MIT" }, - "node_modules/@react-aria/focus": { - "version": "3.19.0", - "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.19.0.tgz", - "integrity": "sha512-hPF9EXoUQeQl1Y21/rbV2H4FdUR2v+4/I0/vB+8U3bT1CJ+1AFj1hc/rqx2DqEwDlEwOHN+E4+mRahQmlybq0A==", - "license": "Apache-2.0", - "dependencies": { - "@react-aria/interactions": "^3.22.5", - "@react-aria/utils": "^3.26.0", - "@react-types/shared": "^3.26.0", - "@swc/helpers": "^0.5.0", - "clsx": "^2.0.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@react-aria/focus/node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@react-aria/interactions": { - "version": "3.22.5", - "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.22.5.tgz", - "integrity": "sha512-kMwiAD9E0TQp+XNnOs13yVJghiy8ET8L0cbkeuTgNI96sOAp/63EJ1FSrDf17iD8sdjt41LafwX/dKXW9nCcLQ==", - "license": "Apache-2.0", - "dependencies": { - "@react-aria/ssr": "^3.9.7", - "@react-aria/utils": "^3.26.0", - "@react-types/shared": "^3.26.0", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@react-aria/interactions/node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@react-aria/ssr": { - "version": "3.9.7", - "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.7.tgz", - "integrity": "sha512-GQygZaGlmYjmYM+tiNBA5C6acmiDWF52Nqd40bBp0Znk4M4hP+LTmI0lpI1BuKMw45T8RIhrAsICIfKwZvi2Gg==", - "license": "Apache-2.0", - "dependencies": { - "@swc/helpers": "^0.5.0" - }, - "engines": { - "node": ">= 12" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@react-aria/ssr/node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@react-aria/utils": { - "version": "3.26.0", - "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.26.0.tgz", - "integrity": "sha512-LkZouGSjjQ0rEqo4XJosS4L3YC/zzQkfRM3KoqK6fUOmUJ9t0jQ09WjiF+uOoG9u+p30AVg3TrZRUWmoTS+koQ==", - "license": "Apache-2.0", - "dependencies": { - "@react-aria/ssr": "^3.9.7", - "@react-stately/utils": "^3.10.5", - "@react-types/shared": "^3.26.0", - "@swc/helpers": "^0.5.0", - "clsx": "^2.0.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@react-aria/utils/node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@react-stately/utils": { - "version": "3.10.5", - "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.5.tgz", - "integrity": "sha512-iMQSGcpaecghDIh3mZEpZfoFH3ExBwTtuBEcvZ2XnGzCgQjeYXcMdIUwAfVQLXFTdHUHGF6Gu6/dFrYsCzySBQ==", - "license": "Apache-2.0", - "dependencies": { - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@react-stately/utils/node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@react-types/shared": { - "version": "3.26.0", - "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.26.0.tgz", - "integrity": "sha512-6FuPqvhmjjlpEDLTiYx29IJCbCNWPlsyO+ZUmCUXzhUv2ttShOXfw8CmeHWHftT/b2KweAWuzqSlfeXPR76jpw==", - "license": "Apache-2.0", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -6466,33 +6419,6 @@ "react": "^18 || ^19" } }, - "node_modules/@tanstack/react-virtual": { - "version": "3.10.9", - "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.10.9.tgz", - "integrity": "sha512-OXO2uBjFqA4Ibr2O3y0YMnkrRWGVNqcvHQXmGvMu6IK8chZl3PrDxFXdGZ2iZkSrKh3/qUYoFqYe+Rx23RoU0g==", - "license": "MIT", - "dependencies": { - "@tanstack/virtual-core": "3.10.9" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@tanstack/react-virtual/node_modules/@tanstack/virtual-core": { - "version": "3.10.9", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.10.9.tgz", - "integrity": "sha512-kBknKOKzmeR7lN+vSadaKWXaLS0SZZG+oqpQ/k80Q6g9REn6zRHS/ZYdrIzHnpHgy/eWs00SujveUN/GJT2qTw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, "node_modules/@testing-library/dom": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", @@ -20036,12 +19962,6 @@ "dev": true, "license": "MIT" }, - "node_modules/tabbable": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", - "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", - "license": "MIT" - }, "node_modules/tailwind-merge": { "version": "2.5.4", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.4.tgz", diff --git a/dashboard/package.json b/dashboard/package.json index 796161f29..61d32d65e 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -12,10 +12,10 @@ "preinstall": "cd ../sdk-js && npm i && npm run build" }, "dependencies": { - "@headlessui/react": "^2.0.3", "@radix-ui/react-accordion": "^1.2.1", "@radix-ui/react-checkbox": "^1.1.1", "@radix-ui/react-dialog": "^1.1.2", + "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.1.0", diff --git a/dashboard/src/app/[tenantId]/(diagram)/components/Modals/ExecuteWorkflowRun.tsx b/dashboard/src/app/[tenantId]/(diagram)/components/Modals/ExecuteWorkflowRun.tsx index 6ee63587a..4d8b613b1 100644 --- a/dashboard/src/app/[tenantId]/(diagram)/components/Modals/ExecuteWorkflowRun.tsx +++ b/dashboard/src/app/[tenantId]/(diagram)/components/Modals/ExecuteWorkflowRun.tsx @@ -68,7 +68,7 @@ export const ExecuteWorkflowRun: FC = ({ data }) => { if (!wfRun.id) return toast.success('Workflow has been executed') setShowModal(false) - router.push(`/wfRun/${wfRun.id.id}`) + router.push(`/${tenantId}/wfRun/${wfRun.id.id}`) } catch (error: any) { toast.error(error.message?.split(':')?.[1]) } diff --git a/dashboard/src/app/[tenantId]/(diagram)/wfSpec/[...props]/components/Thread.tsx b/dashboard/src/app/[tenantId]/(diagram)/wfSpec/[...props]/components/Thread.tsx index be951a33c..0e90d49e6 100644 --- a/dashboard/src/app/[tenantId]/(diagram)/wfSpec/[...props]/components/Thread.tsx +++ b/dashboard/src/app/[tenantId]/(diagram)/wfSpec/[...props]/components/Thread.tsx @@ -1,7 +1,6 @@ 'use client' -import { Disclosure } from '@headlessui/react' +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion' import { ThreadSpec } from 'littlehorse-client/proto' -import { ChevronUpIcon } from 'lucide-react' import { FC } from 'react' import { Mutations } from './Mutations' import { Variables } from './Variables' @@ -13,20 +12,17 @@ type Props = { export const Thread: FC = ({ name, spec }) => { return (
- - {({ open }) => ( - <> - -

Thread: {name}

- -
- - - - - - )} -
+ + + +

Thread: {name}

+
+ + + + +
+
) } 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 dba18d8eb..8347737fa 100644 --- a/dashboard/src/app/[tenantId]/(diagram)/wfSpec/[...props]/components/WfRunsHeader.tsx +++ b/dashboard/src/app/[tenantId]/(diagram)/wfSpec/[...props]/components/WfRunsHeader.tsx @@ -1,5 +1,5 @@ import { TIME_RANGES, TIME_RANGES_NAMES, TimeRange, WF_RUN_STATUSES } from '@/app/constants' -import { Listbox, ListboxButton, ListboxOptions } from '@headlessui/react' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { LHStatus, WfSpec } from 'littlehorse-client/proto' import { ClockIcon } from 'lucide-react' import LinkWithTenant from '@/app/[tenantId]/components/LinkWithTenant' @@ -23,29 +23,21 @@ export const WfRunsHeader: FC = ({ spec, currentStatus, currentWindow, se

WfRun Search

- -
- +
{['ALL', ...WF_RUN_STATUSES].map(status => ( ) { @@ -9,44 +16,19 @@ function classNames(...classes: Array) { export const Principal: FC = () => { const { user } = useWhoAmI() return ( - -
- -
- {user?.name?.at(0)} -
-
-
- - - -
- - {({ active }) => ( - - )} - -
-
-
-
+ + +
+ {user?.name?.at(0)} +
+
+ + {user?.name} + + signOut()} className="block w-full px-4 py-2 text-left text-sm"> + Sign out + + +
) } diff --git a/dashboard/src/app/[tenantId]/components/TenantSelector.tsx b/dashboard/src/app/[tenantId]/components/TenantSelector.tsx index 1a18dcae0..196f13a81 100644 --- a/dashboard/src/app/[tenantId]/components/TenantSelector.tsx +++ b/dashboard/src/app/[tenantId]/components/TenantSelector.tsx @@ -1,8 +1,14 @@ 'use client' import { useWhoAmI } from '@/contexts/WhoAmIContext' -import { Menu, Transition } from '@headlessui/react' +import { + DropdownMenu, + DropdownMenuSeparator, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, +} from '@/components/ui/dropdown-menu' import { useParams, useRouter } from 'next/navigation' -import { FC, Fragment } from 'react' +import { FC } from 'react' export const TenantSelector: FC = () => { const { tenants } = useWhoAmI() @@ -10,53 +16,37 @@ export const TenantSelector: FC = () => { const router = useRouter() return ( - -
- - + + + + + {tenantId} + + +
Tenants
+ + {tenants.map(tenant => ( + router.push(`/${tenant}`)} > - - - {tenantId} -
-
- - -
Tenants
- {tenants.map(tenant => ( -
- - - -
- ))} -
-
-
+ {tenant} + + ))} + + ) } diff --git a/dashboard/src/app/[tenantId]/components/VersionSelector.tsx b/dashboard/src/app/[tenantId]/components/VersionSelector.tsx index e58b0d8b0..1e8aad9ea 100644 --- a/dashboard/src/app/[tenantId]/components/VersionSelector.tsx +++ b/dashboard/src/app/[tenantId]/components/VersionSelector.tsx @@ -1,7 +1,7 @@ -import { Listbox } from '@headlessui/react' import { TagIcon } from 'lucide-react' import { FC } from 'react' import LinkWithTenant from './LinkWithTenant' +import { Select, SelectContent, SelectTrigger, SelectValue } from '@/components/ui/select' type Props = { path: string @@ -12,31 +12,29 @@ type Props = { export const VersionSelector: FC = ({ path, currentVersion, versions, loadVersions }) => { return ( - -
- Version: -
- - - {currentVersion} - - - {[...versions].reverse().map(version => { - return ( - - {version} - - ) - })} - -
-
-
+
+ Version: + +
) } diff --git a/dashboard/src/app/[tenantId]/taskDef/[name]/components/TaskDef.tsx b/dashboard/src/app/[tenantId]/taskDef/[name]/components/TaskDef.tsx index bfbb4df0e..68c8101ed 100644 --- a/dashboard/src/app/[tenantId]/taskDef/[name]/components/TaskDef.tsx +++ b/dashboard/src/app/[tenantId]/taskDef/[name]/components/TaskDef.tsx @@ -5,7 +5,8 @@ import { SearchFooter } from '@/app/[tenantId]/components/SearchFooter' import { SEARCH_DEFAULT_LIMIT } from '@/app/constants' import { concatWfRunIds, localDateTimeToUTCIsoString, utcToLocalDateTime } from '@/app/utils' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' -import { Field, Input, Label } from '@headlessui/react' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' import { useInfiniteQuery } from '@tanstack/react-query' import { TaskDef as TaskDefProto, TaskStatus } from 'littlehorse-client/proto' import { RefreshCwIcon } from 'lucide-react' @@ -64,25 +65,25 @@ export const TaskDef: FC = ({ spec }) => {
- - +
+ ) => setCreatedAfter(e.target.value)} - className="focus:shadow-outline ml-3 w-full appearance-none rounded border px-3 py-2 leading-tight shadow focus:outline-none" + className="w-full" /> - +
- - +
+ ) => setCreatedBefore(e.target.value)} - className="focus:shadow-outline ml-4 w-full appearance-none rounded border px-3 py-2 leading-tight shadow focus:outline-none" + className="w-full" /> - +
{isPending ? ( diff --git a/dashboard/src/app/[tenantId]/userTaskDef/[...props]/components/UserTaskDef.tsx b/dashboard/src/app/[tenantId]/userTaskDef/[...props]/components/UserTaskDef.tsx index 4491acbfd..ade44c9e7 100644 --- a/dashboard/src/app/[tenantId]/userTaskDef/[...props]/components/UserTaskDef.tsx +++ b/dashboard/src/app/[tenantId]/userTaskDef/[...props]/components/UserTaskDef.tsx @@ -11,7 +11,9 @@ import LinkWithTenant from '@/app/[tenantId]/components/LinkWithTenant' import { SEARCH_DEFAULT_LIMIT } from '@/app/constants' import { concatWfRunIds, localDateTimeToUTCIsoString, utcToLocalDateTime } from '@/app/utils' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' -import { Button, Field, Input, Label } from '@headlessui/react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' import { useInfiniteQuery } from '@tanstack/react-query' import { UserTaskDef as UserTaskDefProto, UserTaskRunStatus } from 'littlehorse-client/proto' import { RefreshCwIcon } from 'lucide-react' @@ -85,7 +87,8 @@ export const UserTaskDef: FC = ({ spec }) => { @@ -93,46 +96,46 @@ export const UserTaskDef: FC = ({ spec }) => {
- - +
+ ) => setUserId(e.target.value)} - className="focus:shadow-outline ml-6 w-full appearance-none rounded border px-3 py-2 leading-tight shadow focus:outline-none" + className="w-full" /> - +
- - +
+ ) => setUserGroup(e.target.value)} - className="focus:shadow-outline ml-4 w-full appearance-none rounded border px-3 py-2 leading-tight shadow focus:outline-none" + className="w-full" /> - +
- - +
+ ) => setCreatedAfter(e.target.value)} - className="focus:shadow-outline ml-3 w-full appearance-none rounded border px-3 py-2 leading-tight shadow focus:outline-none" + className="w-full" /> - +
- - +
+ ) => setCreatedBefore(e.target.value)} - className="focus:shadow-outline ml-4 w-full appearance-none rounded border px-3 py-2 leading-tight shadow focus:outline-none" + className="w-full" /> - +
{isPending ? ( diff --git a/dashboard/src/app/constants.ts b/dashboard/src/app/constants.ts index 51b700f1b..e726bac53 100644 --- a/dashboard/src/app/constants.ts +++ b/dashboard/src/app/constants.ts @@ -27,7 +27,7 @@ export const TIME_RANGES = [-1, 5, 15, 30, 60, 180, 360, 720, 1440, 4320] as con export type TimeRange = (typeof TIME_RANGES)[number] export const TIME_RANGES_NAMES: { [key in TimeRange]: string } = { - [-1]: '4.543 billion years', + [-1]: 'All time', 5: '5 minutes', 15: '15 minutes', 30: '30 minutes', diff --git a/dashboard/src/components/ui/accordion.tsx b/dashboard/src/components/ui/accordion.tsx new file mode 100644 index 000000000..99824cd57 --- /dev/null +++ b/dashboard/src/components/ui/accordion.tsx @@ -0,0 +1,54 @@ +'use client' + +import * as React from 'react' +import * as AccordionPrimitive from '@radix-ui/react-accordion' +import { ChevronDown } from 'lucide-react' + +import { cn } from '@/components/utils' + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = 'AccordionItem' + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180', + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)) + +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/dashboard/src/components/ui/dropdown-menu.tsx b/dashboard/src/components/ui/dropdown-menu.tsx new file mode 100644 index 000000000..e567e757e --- /dev/null +++ b/dashboard/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,181 @@ +'use client' + +import * as React from 'react' +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu' +import { Check, ChevronRight, Circle } from 'lucide-react' + +import { cn } from '@/components/utils' + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => { + return +} +DropdownMenuShortcut.displayName = 'DropdownMenuShortcut' + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/dashboard/tailwind.config.ts b/dashboard/tailwind.config.ts index 445e1e8b7..ab2df872f 100644 --- a/dashboard/tailwind.config.ts +++ b/dashboard/tailwind.config.ts @@ -55,12 +55,20 @@ const config = { }, keyframes: { 'accordion-down': { - from: { height: '0' }, - to: { height: 'var(--radix-accordion-content-height)' }, + from: { + height: '0', + }, + to: { + height: 'var(--radix-accordion-content-height)', + }, }, 'accordion-up': { - from: { height: 'var(--radix-accordion-content-height)' }, - to: { height: '0' }, + from: { + height: 'var(--radix-accordion-content-height)', + }, + to: { + height: '0', + }, }, }, animation: { diff --git a/docker/server/Dockerfile b/docker/server/Dockerfile index 6f908ff2a..98c7db2ef 100644 --- a/docker/server/Dockerfile +++ b/docker/server/Dockerfile @@ -1,5 +1,7 @@ FROM amazoncorretto:21 WORKDIR /lh +RUN amazon-linux-extras install epel -y && yum install jemalloc -y +ENV LD_PRELOAD=/usr/lib64/libjemalloc.so.1 COPY ./docker/server/docker-entrypoint.sh /lh COPY ./server/build/libs/server-*-all.jar /lh/server.jar ENTRYPOINT ["/lh/docker-entrypoint.sh"] 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 a9f842522..9a4fb3809 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 @@ -202,7 +202,7 @@ String taskArg1 = "reply to my email, you must!"; String taskArg2 = "for my ally is the Force, and a powerful ally it is"; String taskDefName = "send-reminder"; -wf.scheduleTaskAfter(userTask, delaySeconds, taskDefName, taskArg1, taskArg2); +wf.scheduleReminderTask(userTask, delaySeconds, taskDefName, taskArg1, taskArg2); ``` diff --git a/docs/docs/08-api.md b/docs/docs/08-api.md index 8170029e3..506dcc229 100644 --- a/docs/docs/08-api.md +++ b/docs/docs/08-api.md @@ -3228,7 +3228,7 @@ SDK, which allows the Task Method to determine where the TaskRun comes from. | `node_run_id` | | [NodeRunId](#noderunid) | Is the NodeRun that the UserTaskRun belongs to. | | `user_task_event_number` | | int32 | Is the index in the `events` field of the UserTaskRun that the TaskRun corresponds to. | | `user_id` | optional| string | Is the user_id that the UserTaskRun is assigned to. Unset if UserTaskRun is not asigned to a specific user_id. | -| `user_group` | optional| string | Is the user_id that the UserTaskRun is assigned to. Unset if UserTaskRun is not asigned to a specific user_id. | +| `user_group` | optional| string | Is the user_group that the UserTaskRun is assigned to. Unset if UserTaskRun is not asigned to a specific user_group. | diff --git a/examples/user-tasks/src/main/java/io/littlehorse/examples/UserTasksExample.java b/examples/user-tasks/src/main/java/io/littlehorse/examples/UserTasksExample.java index 0a1d8cc77..54d32fc49 100644 --- a/examples/user-tasks/src/main/java/io/littlehorse/examples/UserTasksExample.java +++ b/examples/user-tasks/src/main/java/io/littlehorse/examples/UserTasksExample.java @@ -40,17 +40,6 @@ public void wf(WorkflowThread wf) { VariableType.BOOL ); - WfRunVariable temperature = wf.addVariable("temperature", VariableType.INT); - WfRunVariable isActive = wf.addVariable("is_active", VariableType.BOOL); - WfRunVariable price = wf.addVariable("price", VariableType.DOUBLE); - WfRunVariable metadata = wf.addVariable("metadata", VariableType.JSON_OBJ); - WfRunVariable tags = wf.addVariable("tags", VariableType.JSON_ARR); - WfRunVariable timestamp = wf.addVariable("timestamp", VariableType.INT); - WfRunVariable description = wf.addVariable("description", VariableType.STR); - WfRunVariable bytes = wf.addVariable("bytes", VariableType.BYTES); - WfRunVariable count = wf.addVariable("count", VariableType.INT); - WfRunVariable enabled = wf.addVariable("enabled", VariableType.BOOL); - // Get the IT Request UserTaskOutput formOutput = wf.assignUserTask( IT_REQUEST_FORM, diff --git a/schemas/littlehorse/user_tasks.proto b/schemas/littlehorse/user_tasks.proto index f414a538c..caf22d190 100644 --- a/schemas/littlehorse/user_tasks.proto +++ b/schemas/littlehorse/user_tasks.proto @@ -214,8 +214,8 @@ message UserTaskTriggerReference { // asigned to a specific user_id. optional string user_id = 3; - // Is the user_id that the UserTaskRun is assigned to. Unset if UserTaskRun is not - // asigned to a specific user_id. + // Is the user_group that the UserTaskRun is assigned to. Unset if UserTaskRun is not + // asigned to a specific user_group. optional string user_group = 4; } diff --git a/sdk-dotnet/Examples/ExceptionsHandlerExample/ExceptionsHandlerExample.csproj b/sdk-dotnet/Examples/ExceptionsHandlerExample/ExceptionsHandlerExample.csproj new file mode 100644 index 000000000..1cb816f31 --- /dev/null +++ b/sdk-dotnet/Examples/ExceptionsHandlerExample/ExceptionsHandlerExample.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/sdk-dotnet/Examples/ExceptionsHandlerExample/Program.cs b/sdk-dotnet/Examples/ExceptionsHandlerExample/Program.cs new file mode 100644 index 000000000..9a135dd8a --- /dev/null +++ b/sdk-dotnet/Examples/ExceptionsHandlerExample/Program.cs @@ -0,0 +1,70 @@ +using ExceptionsHandler; +using LittleHorse.Sdk; +using LittleHorse.Sdk.Worker; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace ExceptionsHandlerExample; + +public abstract class Program +{ + private static ServiceProvider? _serviceProvider; + + private static void SetupApplication() + { + _serviceProvider = new ServiceCollection() + .AddLogging(config => + { + config.AddConsole(); + config.SetMinimumLevel(LogLevel.Debug); + }) + .BuildServiceProvider(); + } + + private static LHConfig GetLHConfig(string[] args, ILoggerFactory loggerFactory) + { + var config = new LHConfig(loggerFactory); + + string filePath = Path.Combine(Directory.GetCurrentDirectory(), ".config/littlehorse.config"); + if (File.Exists(filePath)) + config = new LHConfig(filePath, loggerFactory); + + return config; + } + + private static List> GetTaskWorkers(LHConfig config) + { + MyWorker executableExceptionHandling = new MyWorker(); + var workers = new List> + { + new(executableExceptionHandling, "fail", config), + new(executableExceptionHandling, "fail-new-process", config), + new(executableExceptionHandling, "technical-failure", config), + new(executableExceptionHandling, "my-task", config) + }; + + return workers; + } + + static void Main(string[] args) + { + SetupApplication(); + if (_serviceProvider != null) + { + var loggerFactory = _serviceProvider.GetRequiredService(); + var config = GetLHConfig(args, loggerFactory); + var workers = GetTaskWorkers(config); + foreach (var worker in workers) + { + worker.RegisterTaskDef(); + } + + Thread.Sleep(300); + + foreach (var worker in workers) + { + worker.Start(); + } + } + } +} \ No newline at end of file diff --git a/sdk-dotnet/Examples/ExceptionsHandlerExample/README.md b/sdk-dotnet/Examples/ExceptionsHandlerExample/README.md new file mode 100644 index 000000000..33c8e73ac --- /dev/null +++ b/sdk-dotnet/Examples/ExceptionsHandlerExample/README.md @@ -0,0 +1,31 @@ +## Running Exceptions Handler Example + +This is a simple demonstration of a workflow that handles the failure of a task with +the handleException() functionality, which spawns a child thread and then +resumes execution when the handler thread completes. + +Let's run the example in `ExceptionsHandlerExample` + +``` +dotnet build +dotnet run +``` + +In another terminal, use `lhctl` to run the workflow: + +``` +lhctl run example-exception-handler +``` + +In addition, you can check the result with: + +``` +# This call shows the result +lhctl get wfRun + +# This will show you all nodes in tha run +lhctl list nodeRun + +# This shows the task run information +lhctl get taskRun +``` diff --git a/sdk-dotnet/Examples/ExceptionsHandlerExample/Worker.cs b/sdk-dotnet/Examples/ExceptionsHandlerExample/Worker.cs new file mode 100644 index 000000000..810abaaa3 --- /dev/null +++ b/sdk-dotnet/Examples/ExceptionsHandlerExample/Worker.cs @@ -0,0 +1,56 @@ +using LittleHorse.Common.Proto; +using LittleHorse.Sdk.Worker; +using LHTaskException = LittleHorse.Sdk.Exceptions.LHTaskException; + +namespace ExceptionsHandler +{ + public class MyWorker + { + [LHTaskMethod("fail")] + public void Fail() + { + Random random = new Random(); + int randomNumber = random.Next(6, 10); + var message = $"Throw New Failing Task {randomNumber}."; + if (randomNumber > 5) + { + throw new LHTaskException("Fail", message); + } + + Console.WriteLine(message); + } + + [LHTaskMethod("fail-new-process")] + public void FailNewProcess() + { + Random random = new Random(); + int randomNumber = random.Next(1, 10); + var message = $"Throw Other Failing Task {randomNumber}"; + if (randomNumber < 8) + { + VariableValue content = new VariableValue + { + Str = "This is a problem" + }; + throw new LHTaskException("Fail-New-Task", message, content); + } + + Console.WriteLine(message); + } + + [LHTaskMethod("technical-failure")] + public void FailForTechnicalReason() + { + String message = null!; + int result = message.Length; + Console.WriteLine(result); + } + + [LHTaskMethod("my-task")] + public string PassingTask() + { + Console.WriteLine("Executing passing task."); + return "woohoo!"; + } + } +} \ No newline at end of file diff --git a/sdk-dotnet/Examples/MaskedFieldsExample/MaskedFieldsExample.csproj b/sdk-dotnet/Examples/MaskedFieldsExample/MaskedFieldsExample.csproj index f09e3ff5c..cb535cc93 100644 --- a/sdk-dotnet/Examples/MaskedFieldsExample/MaskedFieldsExample.csproj +++ b/sdk-dotnet/Examples/MaskedFieldsExample/MaskedFieldsExample.csproj @@ -4,7 +4,6 @@ net8.0 enable enable - enable diff --git a/sdk-dotnet/Examples/MaskedFieldsExample/Program.cs b/sdk-dotnet/Examples/MaskedFieldsExample/Program.cs index 7a378c616..4f84d0919 100644 --- a/sdk-dotnet/Examples/MaskedFieldsExample/Program.cs +++ b/sdk-dotnet/Examples/MaskedFieldsExample/Program.cs @@ -1,8 +1,9 @@ -using Examples.BasicExample; using LittleHorse.Sdk; using LittleHorse.Sdk.Worker; -public class Program +namespace MaskedFieldsExample; + +public abstract class Program { private static ServiceProvider? _serviceProvider; private static void SetupApplication() @@ -26,6 +27,19 @@ private static LHConfig GetLHConfig(string[] args, ILoggerFactory loggerFactory) return config; } + + private static List> GetTaskWorkers(LHConfig config) + { + MyWorker executableExceptionHandling = new MyWorker(); + var workers = new List> + { + new(executableExceptionHandling, "create-greet", config), + new(executableExceptionHandling, "update-greet", config), + new(executableExceptionHandling, "delete-greet", config) + }; + + return workers; + } static void Main(string[] args) { @@ -34,23 +48,18 @@ static void Main(string[] args) { var loggerFactory = _serviceProvider.GetRequiredService(); var config = GetLHConfig(args, loggerFactory); + var workers = GetTaskWorkers(config); + foreach (var worker in workers) + { + worker.RegisterTaskDef(); + } - MyWorker executableCreateGreet = new MyWorker(); - var taskWorkerCreate = new LHTaskWorker(executableCreateGreet, "create-greet", config); - MyWorker executableUpdateGreet = new MyWorker(); - var taskWorkerUpdate = new LHTaskWorker(executableUpdateGreet, "update-greet", config); - MyWorker executableDeleteGreet = new MyWorker(); - var taskWorkerDelete = new LHTaskWorker(executableDeleteGreet, "delete-greet", config); - - taskWorkerCreate.RegisterTaskDef(); - taskWorkerUpdate.RegisterTaskDef(); - taskWorkerDelete.RegisterTaskDef(); + Thread.Sleep(300); - Thread.Sleep(1000); - - taskWorkerCreate.Start(); - taskWorkerUpdate.Start(); - taskWorkerDelete.Start(); + foreach (var worker in workers) + { + worker.Start(); + } } } -} +} \ No newline at end of file diff --git a/sdk-dotnet/Examples/MaskedFieldsExample/Worker.cs b/sdk-dotnet/Examples/MaskedFieldsExample/Worker.cs index 9760e3363..c7f6f4e0f 100644 --- a/sdk-dotnet/Examples/MaskedFieldsExample/Worker.cs +++ b/sdk-dotnet/Examples/MaskedFieldsExample/Worker.cs @@ -1,6 +1,6 @@ using LittleHorse.Sdk.Worker; -namespace Examples.BasicExample +namespace MaskedFieldsExample { public class MyWorker { diff --git a/sdk-dotnet/LittleHorse.Sdk.Tests/Helper/LHMappingHelperTest.cs b/sdk-dotnet/LittleHorse.Sdk.Tests/Helper/LHMappingHelperTest.cs index 1a2691164..98fdc081b 100644 --- a/sdk-dotnet/LittleHorse.Sdk.Tests/Helper/LHMappingHelperTest.cs +++ b/sdk-dotnet/LittleHorse.Sdk.Tests/Helper/LHMappingHelperTest.cs @@ -3,7 +3,6 @@ using LittleHorse.Common.Proto; using LittleHorse.Sdk.Helper; using Google.Protobuf.WellKnownTypes; -using LittleHorse.Sdk.Exceptions; using LittleHorse.Sdk.Tests; using Xunit; using Type = System.Type; @@ -84,15 +83,15 @@ public void LHHelper_WithSystemArrayObjectVariableType_ShouldReturnLHVariableJso } [Fact] - public void LHHelper_WithoutSystemVariableType_ShouldThrowException() + public void LHHelper_WithNotAllowedSystemVariableTypes_ShouldReturnLHJsonObj() { - var test_not_allowed_types = new List() { typeof(decimal), typeof(char) }; + var test_not_allowed_types = new List() { typeof(decimal), typeof(char), typeof(void) }; foreach (var type in test_not_allowed_types) { - var exception = Assert.Throws(() => LHMappingHelper.MapDotNetTypeToLHVariableType(type)); + var result = LHMappingHelper.MapDotNetTypeToLHVariableType(type); - Assert.Equal($"Unaccepted variable type.", exception.Message); + Assert.Equal(VariableType.JsonObj, result); } } @@ -145,12 +144,11 @@ public void LHHelper_WithVariableValue_ShouldReturnSameValue() } [Fact] - public void LHHelper_WithNullLHVariableValue_ShouldThrowException() + public void LHHelper_WithNullLHVariableValue_ShouldReturnNewLHVariableValue() { - var exception = Assert.Throws - (() => LHMappingHelper.MapObjectToVariableValue(null)); + var result = LHMappingHelper.MapObjectToVariableValue(null); - Assert.Equal($"There is no object to be mapped.", exception.Message); + Assert.NotNull(result); } [Fact] diff --git a/sdk-dotnet/LittleHorse.Sdk.Tests/LHTaskSignatureTest.cs b/sdk-dotnet/LittleHorse.Sdk.Tests/LHTaskSignatureTest.cs index 2fc89b565..5600a1218 100644 --- a/sdk-dotnet/LittleHorse.Sdk.Tests/LHTaskSignatureTest.cs +++ b/sdk-dotnet/LittleHorse.Sdk.Tests/LHTaskSignatureTest.cs @@ -9,6 +9,7 @@ public class LHTaskSignatureTest { const string TASK_DEF_NAME_ADD = "add-task-worker"; + const string TASK_DEF_NAME_INFORM = "inform-task-worker"; const string TASK_DEF_NAME_UPDATE = "update-task-worker"; const string TASK_DEF_NAME_DELETE = "delete-task-worker"; const string TASK_DEF_NAME_GET = "get-task-worker"; @@ -63,6 +64,32 @@ public void TaskSignature_WithLHTaskMethodAndLHTypeAttributes_ShouldBuildLHSigna Assert.False(taskSignature.HasWorkerContextAtEnd); } + [Fact] + public void TaskSignature_WithoutReturnTypeInLHTaskMethod_ShouldBuildLHSignatureWithoutSchemaOutput() + { + int number_of_method_params = 1; + + var taskSignature = new LHTaskSignature(TASK_DEF_NAME_INFORM, new TestWorker()); + + var expectedLHMethodParam = new LHMethodParam + { + Type = VariableType.Str, + Name = "name", + IsMasked = TRUE_IS_MASKET + }; + + Assert.True(taskSignature.LhMethodParams.Count == number_of_method_params); + foreach (var actualLHMethodParam in taskSignature.LhMethodParams) + { + Assert.Equal(expectedLHMethodParam.Name, actualLHMethodParam.Name); + Assert.Equal(expectedLHMethodParam.Type, actualLHMethodParam.Type); + Assert.Equal(expectedLHMethodParam.IsMasked, actualLHMethodParam.IsMasked); + } + + Assert.Null(taskSignature.TaskDefOutputSchema); + Assert.False(taskSignature.HasWorkerContextAtEnd); + } + [Fact] public void TaskSignature_WithLHTaskMethodAndLHTypeAttributes_ShouldBuildSignatureWithOutputResult() { @@ -202,6 +229,12 @@ public string Add([LHType(masked: TRUE_IS_MASKET)] string name) return $"Output value: {name}"; } + [LHTaskMethod(TASK_DEF_NAME_INFORM)] + public void Inform([LHType(masked: TRUE_IS_MASKET)] string name) + { + var test_variable = "test_variable" + name; + } + [LHTaskMethod(TASK_DEF_NAME_UPDATE)] [LHType(masked: TRUE_IS_MASKET, name: "result")] public string Update(int value) diff --git a/sdk-dotnet/LittleHorse.Sdk/Exceptions/LHTaskException.cs b/sdk-dotnet/LittleHorse.Sdk/Exceptions/LHTaskException.cs new file mode 100644 index 000000000..2c7b8b57e --- /dev/null +++ b/sdk-dotnet/LittleHorse.Sdk/Exceptions/LHTaskException.cs @@ -0,0 +1,22 @@ +using LittleHorse.Common.Proto; + +namespace LittleHorse.Sdk.Exceptions; + +public class LHTaskException: Exception +{ + public string Name { get; } + + public VariableValue Content { get; } + + public LHTaskException(String name, String message): base(message) + { + Name = name; + Content = new VariableValue(); + } + + public LHTaskException(String name, String message, VariableValue content): base(message) + { + Name = name; + Content = content; + } +} \ No newline at end of file diff --git a/sdk-dotnet/LittleHorse.Sdk/Helper/LHMappingHelper.cs b/sdk-dotnet/LittleHorse.Sdk/Helper/LHMappingHelper.cs index 118be46cf..9705fc8a4 100644 --- a/sdk-dotnet/LittleHorse.Sdk/Helper/LHMappingHelper.cs +++ b/sdk-dotnet/LittleHorse.Sdk/Helper/LHMappingHelper.cs @@ -5,6 +5,7 @@ using LittleHorse.Sdk.Exceptions; using LittleHorse.Sdk.Utils; using LittleHorse.Sdk.Worker; +using TaskStatus = LittleHorse.Common.Proto.TaskStatus; using Type = System.Type; namespace LittleHorse.Sdk.Helper @@ -17,34 +18,33 @@ public static VariableType MapDotNetTypeToLHVariableType(Type type) { return VariableType.Int; } - else if (IsFloat(type)) + + if (IsFloat(type)) { return VariableType.Double; } - else if (type.IsAssignableFrom(typeof(string))) + + if (type.IsAssignableFrom(typeof(string))) { return VariableType.Str; } - else if (type.IsAssignableFrom(typeof(bool))) + + if (type.IsAssignableFrom(typeof(bool))) { return VariableType.Bool; } - else if (type.IsAssignableFrom(typeof(byte[]))) + + if (type.IsAssignableFrom(typeof(byte[]))) { return VariableType.Bytes; } - else if (typeof(IEnumerable).IsAssignableFrom(type)) + + if (typeof(IEnumerable).IsAssignableFrom(type)) { return VariableType.JsonArr; } - else if (!type.Namespace!.StartsWith("System")) - { - return VariableType.JsonObj; - } - else - { - throw new Exception("Unaccepted variable type."); - } + + return VariableType.JsonObj; } public static DateTime? MapDateTimeFromProtoTimeStamp(Timestamp protoTimestamp) @@ -67,10 +67,7 @@ public static VariableValue MapObjectToVariableValue(object? obj) if (obj is VariableValue variableValue) return variableValue; var result = new VariableValue(); - if (obj == null) - { - throw new LHInputVarSubstitutionException("There is no object to be mapped."); - } + if (obj == null) {} else if (IsIntObject(obj)) { result.Int = GetIntegralValue(obj); @@ -204,12 +201,34 @@ private static Double GetFloatingValue(object obj) }; } - public static bool isInt64Type(Type type) + public static bool IsInt64Type(Type type) { return type.IsAssignableFrom(typeof(Int64)) || type.IsAssignableFrom(typeof(UInt64)) || type.IsAssignableFrom(typeof(long)) || type.IsAssignableFrom(typeof(ulong)); } + + public static LHErrorType GetFailureCodeFor(TaskStatus status) + { + switch (status) { + case TaskStatus.TaskFailed: + return LHErrorType.TaskFailure; + case TaskStatus.TaskTimeout: + return LHErrorType.Timeout; + case TaskStatus.TaskOutputSerializingError: + return LHErrorType.VarMutationError; + case TaskStatus.TaskInputVarSubError: + return LHErrorType.VarSubError; + case TaskStatus.TaskRunning: + case TaskStatus.TaskScheduled: + case TaskStatus.TaskSuccess: + case TaskStatus.TaskPending: + case TaskStatus.TaskException: // TASK_EXCEPTION is NOT a technical ERROR, so this fails. + break; + } + + throw new ArgumentException($"Unexpected task status: {status}");; + } } } diff --git a/sdk-dotnet/LittleHorse.Sdk/LittleHorse.Sdk.sln b/sdk-dotnet/LittleHorse.Sdk/LittleHorse.Sdk.sln deleted file mode 100644 index cc965db66..000000000 --- a/sdk-dotnet/LittleHorse.Sdk/LittleHorse.Sdk.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.5.002.0 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LittleHorse.Sdk", "LittleHorse.Sdk.csproj", "{F47CBA48-D52D-4DD8-8D17-4F413C3CE469}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {F47CBA48-D52D-4DD8-8D17-4F413C3CE469}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F47CBA48-D52D-4DD8-8D17-4F413C3CE469}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F47CBA48-D52D-4DD8-8D17-4F413C3CE469}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F47CBA48-D52D-4DD8-8D17-4F413C3CE469}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {BBFC2829-71EA-411A-AD2D-D4791D38201D} - EndGlobalSection -EndGlobal diff --git a/sdk-dotnet/LittleHorse.Sdk/Worker/Internal/LHServerConnectionManager.cs b/sdk-dotnet/LittleHorse.Sdk/Worker/Internal/LHServerConnectionManager.cs index 10d3f9bc3..e50882420 100644 --- a/sdk-dotnet/LittleHorse.Sdk/Worker/Internal/LHServerConnectionManager.cs +++ b/sdk-dotnet/LittleHorse.Sdk/Worker/Internal/LHServerConnectionManager.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Logging; using Polly; using static LittleHorse.Common.Proto.LittleHorse; +using LHTaskException = LittleHorse.Sdk.Exceptions.LHTaskException; using TaskStatus = LittleHorse.Common.Proto.TaskStatus; namespace LittleHorse.Sdk.Worker.Internal @@ -218,18 +219,56 @@ private ReportTaskRun ExecuteTask(ScheduledTask scheduledTask, DateTime? schedul _logger?.LogError(ex, "Failed calculating task input variables"); taskResult.LogOutput = LHMappingHelper.MapExceptionToVariableValue(ex, workerContext); taskResult.Status = TaskStatus.TaskInputVarSubError; + taskResult.Error = new LHTaskError + { + Message = ex.ToString(), Type = LHMappingHelper.GetFailureCodeFor(taskResult.Status) + }; } catch (LHSerdeException ex) { _logger?.LogError(ex, "Failed serializing Task Output"); taskResult.LogOutput = LHMappingHelper.MapExceptionToVariableValue(ex, workerContext); taskResult.Status = TaskStatus.TaskOutputSerializingError; + taskResult.Error = new LHTaskError + { + Message = ex.ToString(), Type = LHMappingHelper.GetFailureCodeFor(taskResult.Status) + }; + } + catch (TargetInvocationException ex) + { + if (ex.GetBaseException() is LHTaskException taskException) + { + _logger?.LogError(ex, "Task Method threw a Business Exception"); + taskResult.LogOutput = LHMappingHelper.MapExceptionToVariableValue(ex, workerContext); + taskResult.Status = TaskStatus.TaskException; + taskResult.Exception = new Common.Proto.LHTaskException + { + Name = taskException.Name, + Message = taskException.Message, + Content = taskException.Content + }; + } + else + { + _logger?.LogError(ex, "Task Method threw an exception"); + taskResult.LogOutput = LHMappingHelper.MapExceptionToVariableValue(ex, workerContext); + taskResult.Status = TaskStatus.TaskFailed; + taskResult.Error = new LHTaskError + { + Message = ex.InnerException!.ToString(), + Type = LHMappingHelper.GetFailureCodeFor(taskResult.Status) + }; + } } catch (Exception ex) { _logger?.LogError(ex, "Unexpected exception during task execution"); taskResult.LogOutput = LHMappingHelper.MapExceptionToVariableValue(ex, workerContext); taskResult.Status = TaskStatus.TaskFailed; + taskResult.Error = new LHTaskError + { + Message = ex.ToString(), Type = LHMappingHelper.GetFailureCodeFor(taskResult.Status) + }; } taskResult.Time = Timestamp.FromDateTime(DateTime.UtcNow); diff --git a/sdk-dotnet/LittleHorse.Sdk/Worker/LHTaskSignature.cs b/sdk-dotnet/LittleHorse.Sdk/Worker/LHTaskSignature.cs index ba3ead229..a9a6f7f01 100644 --- a/sdk-dotnet/LittleHorse.Sdk/Worker/LHTaskSignature.cs +++ b/sdk-dotnet/LittleHorse.Sdk/Worker/LHTaskSignature.cs @@ -50,9 +50,10 @@ public LHTaskSignature(string taskDefName, T executable) var methodParams = TaskMethod.GetParameters(); - CreateInputVarsSignature(methodParams); + BuildInputVarsSignature(methodParams); - CreateOutputSchemaSignature(); + if (!TaskMethod.ReturnType.IsAssignableFrom(typeof(void))) + BuildOutputSchemaSignature(); } private bool IsValidLHTaskWorkerValue(Attribute? lhtaskWorkerAttribute, string taskDefName) @@ -65,7 +66,7 @@ private bool IsValidLHTaskWorkerValue(Attribute? lhtaskWorkerAttribute, string t return false; } - private void CreateInputVarsSignature(ParameterInfo[] methodParams) + private void BuildInputVarsSignature(ParameterInfo[] methodParams) { for (int i = 0; i < methodParams.Length; i++) { @@ -107,7 +108,7 @@ private void CreateInputVarsSignature(ParameterInfo[] methodParams) } } - private void CreateOutputSchemaSignature() + private void BuildOutputSchemaSignature() { var returnType = LHMappingHelper.MapDotNetTypeToLHVariableType(TaskMethod.ReturnType); var maskedValue = false; diff --git a/sdk-dotnet/LittleHorse.Sdk/Worker/VariableMapping.cs b/sdk-dotnet/LittleHorse.Sdk/Worker/VariableMapping.cs index 36514461b..02e958833 100644 --- a/sdk-dotnet/LittleHorse.Sdk/Worker/VariableMapping.cs +++ b/sdk-dotnet/LittleHorse.Sdk/Worker/VariableMapping.cs @@ -32,7 +32,7 @@ public VariableMapping(TaskDef taskDef, int position, Type type, string? paramNa public object? Assign(ScheduledTask taskInstance, LHWorkerContext workerContext) { - if (_type.GetType() == typeof(LHWorkerContext)) + if (_type == typeof(LHWorkerContext)) { return workerContext; } @@ -45,7 +45,7 @@ public VariableMapping(TaskDef taskDef, int position, Type type, string? paramNa switch (val.ValueCase) { case VariableValue.ValueOneofCase.Int: - if (LHMappingHelper.isInt64Type(_type)) + if (LHMappingHelper.IsInt64Type(_type)) { return val.Int; } diff --git a/sdk-dotnet/Littlehorse.sln b/sdk-dotnet/Littlehorse.sln index 0b488ec5f..23959b58f 100644 --- a/sdk-dotnet/Littlehorse.sln +++ b/sdk-dotnet/Littlehorse.sln @@ -11,6 +11,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BasicExample", "Examples\Ba EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaskedFieldsExample", "Examples\MaskedFieldsExample\MaskedFieldsExample.csproj", "{AA9767C3-9F03-4414-A4C2-A0C3761A975C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LittleHorse.Sdk.Tests", "LittleHorse.Sdk.Tests\LittleHorse.Sdk.Tests.csproj", "{AC276D3F-ABAB-4E2D-B721-099681083390}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExceptionsHandlerExample", "Examples\ExceptionsHandlerExample\ExceptionsHandlerExample.csproj", "{6B7A8034-21C0-465D-8708-4F47A1780972}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -32,9 +36,18 @@ Global {AA9767C3-9F03-4414-A4C2-A0C3761A975C}.Debug|Any CPU.Build.0 = Debug|Any CPU {AA9767C3-9F03-4414-A4C2-A0C3761A975C}.Release|Any CPU.ActiveCfg = Release|Any CPU {AA9767C3-9F03-4414-A4C2-A0C3761A975C}.Release|Any CPU.Build.0 = Release|Any CPU + {AC276D3F-ABAB-4E2D-B721-099681083390}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AC276D3F-ABAB-4E2D-B721-099681083390}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AC276D3F-ABAB-4E2D-B721-099681083390}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AC276D3F-ABAB-4E2D-B721-099681083390}.Release|Any CPU.Build.0 = Release|Any CPU + {6B7A8034-21C0-465D-8708-4F47A1780972}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6B7A8034-21C0-465D-8708-4F47A1780972}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6B7A8034-21C0-465D-8708-4F47A1780972}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6B7A8034-21C0-465D-8708-4F47A1780972}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {88CB23B3-1174-45F5-9F72-46AB57BAB661} = {EEC3BB78-BFA5-4525-A965-4EAA3455BBAE} {AA9767C3-9F03-4414-A4C2-A0C3761A975C} = {EEC3BB78-BFA5-4525-A965-4EAA3455BBAE} + {6B7A8034-21C0-465D-8708-4F47A1780972} = {EEC3BB78-BFA5-4525-A965-4EAA3455BBAE} EndGlobalSection EndGlobal diff --git a/sdk-go/lhproto/user_tasks.pb.go b/sdk-go/lhproto/user_tasks.pb.go index 52e588a08..6fbe3af93 100644 --- a/sdk-go/lhproto/user_tasks.pb.go +++ b/sdk-go/lhproto/user_tasks.pb.go @@ -770,8 +770,8 @@ type UserTaskTriggerReference struct { // Is the user_id that the UserTaskRun is assigned to. Unset if UserTaskRun is not // asigned to a specific user_id. UserId *string `protobuf:"bytes,3,opt,name=user_id,json=userId,proto3,oneof" json:"user_id,omitempty"` - // Is the user_id that the UserTaskRun is assigned to. Unset if UserTaskRun is not - // asigned to a specific user_id. + // Is the user_group that the UserTaskRun is assigned to. Unset if UserTaskRun is not + // asigned to a specific user_group. UserGroup *string `protobuf:"bytes,4,opt,name=user_group,json=userGroup,proto3,oneof" json:"user_group,omitempty"` } diff --git a/sdk-java/src/main/java/io/littlehorse/sdk/common/proto/UserTaskTriggerReference.java b/sdk-java/src/main/java/io/littlehorse/sdk/common/proto/UserTaskTriggerReference.java index d178ad079..78456a5fe 100644 --- a/sdk-java/src/main/java/io/littlehorse/sdk/common/proto/UserTaskTriggerReference.java +++ b/sdk-java/src/main/java/io/littlehorse/sdk/common/proto/UserTaskTriggerReference.java @@ -174,8 +174,8 @@ public java.lang.String getUserId() { private volatile java.lang.Object userGroup_ = ""; /** *
-   * Is the user_id that the UserTaskRun is assigned to. Unset if UserTaskRun is not
-   * asigned to a specific user_id.
+   * Is the user_group that the UserTaskRun is assigned to. Unset if UserTaskRun is not
+   * asigned to a specific user_group.
    * 
* * optional string user_group = 4; @@ -187,8 +187,8 @@ public boolean hasUserGroup() { } /** *
-   * Is the user_id that the UserTaskRun is assigned to. Unset if UserTaskRun is not
-   * asigned to a specific user_id.
+   * Is the user_group that the UserTaskRun is assigned to. Unset if UserTaskRun is not
+   * asigned to a specific user_group.
    * 
* * optional string user_group = 4; @@ -209,8 +209,8 @@ public java.lang.String getUserGroup() { } /** *
-   * Is the user_id that the UserTaskRun is assigned to. Unset if UserTaskRun is not
-   * asigned to a specific user_id.
+   * Is the user_group that the UserTaskRun is assigned to. Unset if UserTaskRun is not
+   * asigned to a specific user_group.
    * 
* * optional string user_group = 4; @@ -980,8 +980,8 @@ public Builder setUserIdBytes( private java.lang.Object userGroup_ = ""; /** *
-     * Is the user_id that the UserTaskRun is assigned to. Unset if UserTaskRun is not
-     * asigned to a specific user_id.
+     * Is the user_group that the UserTaskRun is assigned to. Unset if UserTaskRun is not
+     * asigned to a specific user_group.
      * 
* * optional string user_group = 4; @@ -992,8 +992,8 @@ public boolean hasUserGroup() { } /** *
-     * Is the user_id that the UserTaskRun is assigned to. Unset if UserTaskRun is not
-     * asigned to a specific user_id.
+     * Is the user_group that the UserTaskRun is assigned to. Unset if UserTaskRun is not
+     * asigned to a specific user_group.
      * 
* * optional string user_group = 4; @@ -1013,8 +1013,8 @@ public java.lang.String getUserGroup() { } /** *
-     * Is the user_id that the UserTaskRun is assigned to. Unset if UserTaskRun is not
-     * asigned to a specific user_id.
+     * Is the user_group that the UserTaskRun is assigned to. Unset if UserTaskRun is not
+     * asigned to a specific user_group.
      * 
* * optional string user_group = 4; @@ -1035,8 +1035,8 @@ public java.lang.String getUserGroup() { } /** *
-     * Is the user_id that the UserTaskRun is assigned to. Unset if UserTaskRun is not
-     * asigned to a specific user_id.
+     * Is the user_group that the UserTaskRun is assigned to. Unset if UserTaskRun is not
+     * asigned to a specific user_group.
      * 
* * optional string user_group = 4; @@ -1053,8 +1053,8 @@ public Builder setUserGroup( } /** *
-     * Is the user_id that the UserTaskRun is assigned to. Unset if UserTaskRun is not
-     * asigned to a specific user_id.
+     * Is the user_group that the UserTaskRun is assigned to. Unset if UserTaskRun is not
+     * asigned to a specific user_group.
      * 
* * optional string user_group = 4; @@ -1068,8 +1068,8 @@ public Builder clearUserGroup() { } /** *
-     * Is the user_id that the UserTaskRun is assigned to. Unset if UserTaskRun is not
-     * asigned to a specific user_id.
+     * Is the user_group that the UserTaskRun is assigned to. Unset if UserTaskRun is not
+     * asigned to a specific user_group.
      * 
* * optional string user_group = 4; diff --git a/sdk-java/src/main/java/io/littlehorse/sdk/common/proto/UserTaskTriggerReferenceOrBuilder.java b/sdk-java/src/main/java/io/littlehorse/sdk/common/proto/UserTaskTriggerReferenceOrBuilder.java index 3052f74d3..01504da4d 100644 --- a/sdk-java/src/main/java/io/littlehorse/sdk/common/proto/UserTaskTriggerReferenceOrBuilder.java +++ b/sdk-java/src/main/java/io/littlehorse/sdk/common/proto/UserTaskTriggerReferenceOrBuilder.java @@ -79,8 +79,8 @@ public interface UserTaskTriggerReferenceOrBuilder extends /** *
-   * Is the user_id that the UserTaskRun is assigned to. Unset if UserTaskRun is not
-   * asigned to a specific user_id.
+   * Is the user_group that the UserTaskRun is assigned to. Unset if UserTaskRun is not
+   * asigned to a specific user_group.
    * 
* * optional string user_group = 4; @@ -89,8 +89,8 @@ public interface UserTaskTriggerReferenceOrBuilder extends boolean hasUserGroup(); /** *
-   * Is the user_id that the UserTaskRun is assigned to. Unset if UserTaskRun is not
-   * asigned to a specific user_id.
+   * Is the user_group that the UserTaskRun is assigned to. Unset if UserTaskRun is not
+   * asigned to a specific user_group.
    * 
* * optional string user_group = 4; @@ -99,8 +99,8 @@ public interface UserTaskTriggerReferenceOrBuilder extends java.lang.String getUserGroup(); /** *
-   * Is the user_id that the UserTaskRun is assigned to. Unset if UserTaskRun is not
-   * asigned to a specific user_id.
+   * Is the user_group that the UserTaskRun is assigned to. Unset if UserTaskRun is not
+   * asigned to a specific user_group.
    * 
* * optional string user_group = 4; diff --git a/sdk-java/src/main/java/io/littlehorse/sdk/wfsdk/WfRunVariable.java b/sdk-java/src/main/java/io/littlehorse/sdk/wfsdk/WfRunVariable.java index a5a1002a5..7f17f0ef3 100644 --- a/sdk-java/src/main/java/io/littlehorse/sdk/wfsdk/WfRunVariable.java +++ b/sdk-java/src/main/java/io/littlehorse/sdk/wfsdk/WfRunVariable.java @@ -214,5 +214,5 @@ public interface WfRunVariable extends LHExpression { * provided by the Json Path is mutated. * @param rhs is the value to set this WfRunVariable to. */ - void assignTo(Serializable rhs); + void assign(Serializable rhs); } diff --git a/sdk-java/src/main/java/io/littlehorse/sdk/wfsdk/internal/WfRunVariableImpl.java b/sdk-java/src/main/java/io/littlehorse/sdk/wfsdk/internal/WfRunVariableImpl.java index 48f6bd5ec..4f408af79 100644 --- a/sdk-java/src/main/java/io/littlehorse/sdk/wfsdk/internal/WfRunVariableImpl.java +++ b/sdk-java/src/main/java/io/littlehorse/sdk/wfsdk/internal/WfRunVariableImpl.java @@ -37,9 +37,14 @@ class WfRunVariableImpl implements WfRunVariable { public WfRunVariableImpl(String name, Object typeOrDefaultVal, WorkflowThreadImpl parent) { this.name = name; - this.typeOrDefaultVal = typeOrDefaultVal; this.parent = parent; + if (typeOrDefaultVal == null) { + throw new IllegalArgumentException( + "The 'typeOrDefaultVal' argument must be either a VariableType or a default value, but a null value was provided."); + } + this.typeOrDefaultVal = typeOrDefaultVal; + // As per GH Issue #582, the default is now PRIVATE_VAR. this.accessLevel = WfRunVariableAccessLevel.PRIVATE_VAR; initializeType(); @@ -49,13 +54,8 @@ private void initializeType() { if (typeOrDefaultVal instanceof VariableType) { this.type = (VariableType) typeOrDefaultVal; } else { - try { - this.defaultValue = LHLibUtil.objToVarVal(typeOrDefaultVal); - this.type = LHLibUtil.fromValueCase(defaultValue.getValueCase()); - } catch (LHSerdeError e) { - throw new IllegalArgumentException( - "Was unable to convert provided default value to LH Variable Type", e); - } + setDefaultValue(typeOrDefaultVal); + this.type = LHLibUtil.fromValueCase(defaultValue.getValueCase()); } } @@ -98,15 +98,21 @@ public WfRunVariable required() { @Override public WfRunVariable withDefault(Object defaultVal) { + setDefaultValue(defaultVal); + + if (!LHLibUtil.fromValueCase(defaultValue.getValueCase()).equals(type)) { + throw new IllegalArgumentException("Default value type does not match LH variable type " + type); + } + + return this; + } + + private void setDefaultValue(Object defaultVal) { try { - VariableValue attempt = LHLibUtil.objToVarVal(defaultVal); - if (!LHLibUtil.fromValueCase(attempt.getValueCase()).equals(type)) { - throw new IllegalArgumentException("Default value type does not match variable type"); - } + this.defaultValue = LHLibUtil.objToVarVal(defaultVal); } catch (LHSerdeError e) { throw new IllegalArgumentException("Was unable to convert provided default value to LH Variable Type", e); } - return this; } @Override @@ -175,7 +181,7 @@ public WorkflowConditionImpl isNotIn(Serializable rhs) { } @Override - public void assignTo(Serializable rhs) { + public void assign(Serializable rhs) { parent.mutate(this, VariableMutationType.ASSIGN, rhs); } diff --git a/sdk-java/src/main/java/io/littlehorse/sdk/wfsdk/internal/taskdefutil/LHTaskSignature.java b/sdk-java/src/main/java/io/littlehorse/sdk/wfsdk/internal/taskdefutil/LHTaskSignature.java index 7a71af93c..7fbe9e067 100644 --- a/sdk-java/src/main/java/io/littlehorse/sdk/wfsdk/internal/taskdefutil/LHTaskSignature.java +++ b/sdk-java/src/main/java/io/littlehorse/sdk/wfsdk/internal/taskdefutil/LHTaskSignature.java @@ -60,24 +60,17 @@ public LHTaskSignature(String taskDefName, Object executable, String lhTaskMetho + " on " + executable.getClass()); } - VariableType returnType = LHLibUtil.javaClassToLHVarType(taskMethod.getReturnType()); - boolean maskedValue = false; - String outputSchemaVarName = "output"; - if (taskMethod.isAnnotationPresent(LHType.class)) { - LHType type = taskMethod.getAnnotation(LHType.class); - maskedValue = type.masked(); - if (!type.name().isEmpty() || !type.name().isBlank()) { - outputSchemaVarName = type.name(); - } + + Class returnType = taskMethod.getReturnType(); + + buildInputVarsSignature(); + + if (!void.class.isAssignableFrom(returnType)) { + buildOutputSchemaSignature(returnType); } - outputSchema = TaskDefOutputSchema.newBuilder() - .setValueDef(VariableDef.newBuilder() - .setType(returnType) - .setName(outputSchemaVarName) - .setMaskedValue(maskedValue) - .build()) - .build(); + } + private void buildInputVarsSignature() { for (int i = 0; i < taskMethod.getParameterCount(); i++) { Parameter param = taskMethod.getParameters()[i]; if (param.getType().equals(WorkerContext.class)) { @@ -105,6 +98,26 @@ public LHTaskSignature(String taskDefName, Object executable, String lhTaskMetho } } + private void buildOutputSchemaSignature(Class classReturnType) { + VariableType returnType = LHLibUtil.javaClassToLHVarType(classReturnType); + boolean maskedValue = false; + String outputSchemaVarName = "output"; + if (taskMethod.isAnnotationPresent(LHType.class)) { + LHType type = taskMethod.getAnnotation(LHType.class); + maskedValue = type.masked(); + if (!type.name().isEmpty() || !type.name().isBlank()) { + outputSchemaVarName = type.name(); + } + } + outputSchema = TaskDefOutputSchema.newBuilder() + .setValueDef(VariableDef.newBuilder() + .setType(returnType) + .setName(outputSchemaVarName) + .setMaskedValue(maskedValue) + .build()) + .build(); + } + private String varNameFromParameterName(Parameter param) { if (!param.isNamePresent()) { log.warn( diff --git a/sdk-java/src/test/java/io/littlehorse/sdk/wfsdk/internal/WorkflowThreadImplTest.java b/sdk-java/src/test/java/io/littlehorse/sdk/wfsdk/internal/WorkflowThreadImplTest.java index 681c6a666..2a8aa238e 100644 --- a/sdk-java/src/test/java/io/littlehorse/sdk/wfsdk/internal/WorkflowThreadImplTest.java +++ b/sdk-java/src/test/java/io/littlehorse/sdk/wfsdk/internal/WorkflowThreadImplTest.java @@ -550,7 +550,7 @@ void mutationsShouldUseVariableAssignment() { // Deprecated the literal_value and node_output approach Workflow workflow = new WorkflowImpl("obiwan", wf -> { WfRunVariable myVar = wf.addVariable("my-var", VariableType.STR); - myVar.assignTo("some-value"); + myVar.assign("some-value"); }); PutWfSpecRequest wfSpec = workflow.compileWorkflow(); @@ -570,7 +570,7 @@ void nodeOutputMutationsShouldAlsoUseVariableAssignments() { // Deprecated the literal_value and node_output approach Workflow workflow = new WorkflowImpl("obiwan", wf -> { WfRunVariable myVar = wf.addVariable("my-var", VariableType.STR); - myVar.assignTo(wf.execute("use-the-force")); + myVar.assign(wf.execute("use-the-force")); }); PutWfSpecRequest wfSpec = workflow.compileWorkflow(); @@ -590,7 +590,7 @@ void nodeOutputMutationsShouldCarryJsonPath() { // Deprecated the literal_value and node_output approach Workflow workflow = new WorkflowImpl("obiwan", wf -> { WfRunVariable myVar = wf.addVariable("my-var", VariableType.STR); - myVar.assignTo(wf.execute("use-the-force").jsonPath("$.hello.there")); + myVar.assign(wf.execute("use-the-force").jsonPath("$.hello.there")); }); PutWfSpecRequest wfSpec = workflow.compileWorkflow(); @@ -614,7 +614,7 @@ void assigningVariablesToOtherVariablesShouldUseVariableAssignment() { Workflow workflow = new WorkflowImpl("obiwan", wf -> { WfRunVariable myVar = wf.addVariable("my-var", VariableType.STR); WfRunVariable otherVar = wf.addVariable("other-var", VariableType.STR); - myVar.assignTo(otherVar); + myVar.assign(otherVar); }); PutWfSpecRequest wfSpec = workflow.compileWorkflow(); @@ -632,7 +632,7 @@ void assigningVariablesToOtherVariablesShouldCarryJsonPath() { Workflow workflow = new WorkflowImpl("obiwan", wf -> { WfRunVariable myVar = wf.addVariable("my-var", VariableType.STR); WfRunVariable otherVar = wf.addVariable("other-var", VariableType.JSON_OBJ); - myVar.assignTo(otherVar.jsonPath("$.hello.there")); + myVar.assign(otherVar.jsonPath("$.hello.there")); }); PutWfSpecRequest wfSpec = workflow.compileWorkflow(); diff --git a/sdk-js/src/proto/user_tasks.ts b/sdk-js/src/proto/user_tasks.ts index 74dc7bd9a..45ffd4eaf 100644 --- a/sdk-js/src/proto/user_tasks.ts +++ b/sdk-js/src/proto/user_tasks.ts @@ -347,8 +347,8 @@ export interface UserTaskTriggerReference { | string | undefined; /** - * Is the user_id that the UserTaskRun is assigned to. Unset if UserTaskRun is not - * asigned to a specific user_id. + * Is the user_group that the UserTaskRun is assigned to. Unset if UserTaskRun is not + * asigned to a specific user_group. */ userGroup?: string | undefined; } diff --git a/sdk-python/examples/basic/example_basic.py b/sdk-python/examples/basic/example_basic.py index bed79022f..fe349646c 100644 --- a/sdk-python/examples/basic/example_basic.py +++ b/sdk-python/examples/basic/example_basic.py @@ -46,4 +46,4 @@ async def main() -> None: if __name__ == "__main__": - asyncio.run(main()) + asyncio.run(main()) \ No newline at end of file diff --git a/sdk-python/littlehorse/model/__init__.py b/sdk-python/littlehorse/model/__init__.py index c956189c7..85f573b7e 100644 --- a/sdk-python/littlehorse/model/__init__.py +++ b/sdk-python/littlehorse/model/__init__.py @@ -5,8 +5,8 @@ from .node_run_pb2 import * from .object_id_pb2 import * from .scheduled_wf_run_pb2 import * -from .service_pb2_grpc import * from .service_pb2 import * +from .service_pb2_grpc import * from .task_def_pb2 import * from .task_run_pb2 import * from .user_tasks_pb2 import * diff --git a/sdk-python/littlehorse/workflow.py b/sdk-python/littlehorse/workflow.py index 13b54058a..74d6e72d9 100644 --- a/sdk-python/littlehorse/workflow.py +++ b/sdk-python/littlehorse/workflow.py @@ -130,12 +130,62 @@ def to_variable_assignment(value: Any) -> VariableAssignment: json_path=json_path, variable_name=variable_name, ) + + if isinstance(value, LHExpression): + expression: LHExpression = value + return VariableAssignment( + expression=VariableAssignment.Expression( + lhs=to_variable_assignment(expression.lhs()), + operation=expression.operation(), + rhs=to_variable_assignment(expression.rhs())) + ) return VariableAssignment( literal_value=to_variable_value(value), ) +class LHExpression: + def __init__(self, lhs: Any, operation: VariableMutationType, rhs: Any) -> None: + self._lhs = lhs + self._operation = operation + self._rhs = rhs + + def add(self, other: Any) -> LHExpression: + return LHExpression(self, VariableMutationType.ADD, other) + + def subtract(self, other: Any) -> LHExpression: + return LHExpression(self, VariableMutationType.SUBTRACT, other) + + def multiply(self, other: Any) -> LHExpression: + return LHExpression(self, VariableMutationType.MULTIPLY, other) + + def divide(self, other: Any) -> LHExpression: + return LHExpression(self, VariableMutationType.DIVIDE, other) + + def extend(self, other: Any) -> LHExpression: + return LHExpression(self, VariableMutationType.EXTEND, other) + + def remove_if_present(self, other: Any) -> LHExpression: + return LHExpression(self, VariableMutationType.REMOVE_IF_PRESENT, other) + + def remove_index(self, index: Optional[Union[int, Any]] = None) -> LHExpression: + if index is None: + raise ValueError("Expected 'index' to be set, but it was None.") + return LHExpression(self, VariableMutationType.REMOVE_INDEX, index) + + def remove_key(self, other: Any) -> LHExpression: + return LHExpression(self, VariableMutationType.REMOVE_KEY, other) + + def lhs(self) -> Any: + return self._lhs + + def rhs(self) -> Any: + return self._rhs + + def operation(self) -> Any: + return self._operation + class WorkflowCondition: def __init__(self, left_hand: Any, comparator: Comparator, right_hand: Any) -> None: """Returns a WorkflowCondition that can be used in @@ -246,7 +296,7 @@ def __init__(self, format: str, *args: Any) -> None: self._args = args -class NodeOutput: +class NodeOutput(LHExpression): def __init__(self, node_name: str) -> None: self.node_name = node_name self._json_path: Optional[str] = None @@ -284,6 +334,32 @@ def with_json_path(self, json_path: str) -> "NodeOutput": out = NodeOutput(self.node_name) out.json_path = json_path return out + + def add(self, other: Any) -> LHExpression: + return LHExpression(self, VariableMutationType.ADD, other) + + def subtract(self, other: Any) -> LHExpression: + return LHExpression(self, VariableMutationType.SUBTRACT, other) + + def multiply(self, other: Any) -> LHExpression: + return LHExpression(self, VariableMutationType.MULTIPLY, other) + + def divide(self, other: Any) -> LHExpression: + return LHExpression(self, VariableMutationType.DIVIDE, other) + + def extend(self, other: Any) -> LHExpression: + return LHExpression(self, VariableMutationType.EXTEND, other) + + def remove_if_present(self, other: Any) -> LHExpression: + return LHExpression(self, VariableMutationType.REMOVE_IF_PRESENT, other) + + def remove_index(self, index: Optional[Union[int, Any]] = None) -> LHExpression: + if index is None: + raise ValueError("Expected 'index' to be set, but it was None.") + return LHExpression(self, VariableMutationType.REMOVE_INDEX, index) + + def remove_key(self, other: Any) -> LHExpression: + return LHExpression(self, VariableMutationType.REMOVE_KEY, other) class WfRunVariable: @@ -291,18 +367,20 @@ def __init__( self, variable_name: str, variable_type: VariableType, + parent: WorkflowThread, default_value: Any = None, access_level: Optional[ Union[WfRunVariableAccessLevel, str] - ] = WfRunVariableAccessLevel.PUBLIC_VAR, + ] = WfRunVariableAccessLevel.PRIVATE_VAR, ) -> None: """Defines a Variable in the ThreadSpec and returns a handle to it. Args: variable_name (str): The name of the variable. variable_type (VariableType): The variable type. - access_level (WfRunVariableAccessLevel): Sets the access level of a WfRunVariable. + parent (WorkflowThread): The parent WorkflowThread of this WfRunVariable. default_value (Any, optional): A default value. Defaults to None. + access_level (WfRunVariableAccessLevel): Sets the access level of a WfRunVariable. Defaults to PRIVATE_VAR. Returns: WfRunVariable: A handle to the created WfRunVariable. @@ -311,7 +389,8 @@ def __init__( TypeError: If variable_type and type(default_value) are not compatible. """ self.name = variable_name - self.type = variable_type + self.type = variable_type + self.parent = parent self.default_value: Optional[VariableValue] = None self._json_path: Optional[str] = None self._required = False @@ -321,14 +400,7 @@ def __init__( self._access_level = access_level if default_value is not None: - self.default_value = to_variable_value(default_value) - if ( - self.default_value.WhichOneof("value") - != str(VariableType.Name(self.type)).lower() - ): - raise TypeError( - f"Default value is not a {VariableType.Name(variable_type)}" - ) + self._set_default(default_value) @property def json_path(self) -> Optional[str]: @@ -368,7 +440,7 @@ def with_json_path(self, json_path: str) -> "WfRunVariable": f"JsonPath not allowed in a {VariableType.Name(self.type)} variable" ) - out = WfRunVariable(self.name, self.type, self.default_value) + out = WfRunVariable(self.name, self.type, self.parent, self.default_value) out.json_path = json_path return out @@ -445,6 +517,21 @@ def searchable_on( def required(self) -> "WfRunVariable": self._required = True return self + + def with_default(self, default_value: Any) -> WfRunVariable: + self._set_default(default_value) + + return self + + def _set_default(self, default_value: Any) -> None: + self.default_value = to_variable_value(default_value) + if ( + self.default_value.WhichOneof("value") + != str(VariableType.Name(self.type)).lower() + ): + raise TypeError( + f"Default value type does not match LH variable type {VariableType.Name(self.type)}" + ) def masked(self) -> "WfRunVariable": self._masked = True @@ -469,6 +556,65 @@ def compile(self) -> ThreadVarDef: access_level=self._access_level, ) + def is_equal_to(self, rhs: Any) -> WorkflowCondition: + return self.parent.condition(self, Comparator.EQUALS, rhs) + + def is_not_equal_to(self, rhs: Any) -> WorkflowCondition: + return self.parent.condition(self, Comparator.NOT_EQUALS, rhs) + + def is_greater_than(self, rhs: Any) -> WorkflowCondition: + return self.parent.condition(self, Comparator.GREATER_THAN, rhs) + + def is_greater_than_eq(self, rhs: Any) -> WorkflowCondition: + return self.parent.condition(self, Comparator.GREATER_THAN_EQ, rhs) + + def is_less_than_eq(self, rhs: Any) -> WorkflowCondition: + return self.parent.condition(self, Comparator.LESS_THAN_EQ, rhs) + + def is_less_than(self, rhs: Any) -> WorkflowCondition: + return self.parent.condition(self, Comparator.LESS_THAN, rhs) + + def does_contain(self, rhs: Any) -> WorkflowCondition: + return self.parent.condition(self, Comparator.IN, rhs) + + def does_not_contain(self, rhs: Any) -> WorkflowCondition: + return self.parent.condition(self, Comparator.NOT_IN, rhs) + + def is_in(self, rhs: Any) -> WorkflowCondition: + return self.parent.condition(self, Comparator.IN, rhs) + + def is_not_in(self, rhs: Any) -> WorkflowCondition: + return self.parent.condition(self, Comparator.NOT_IN, rhs) + + def assign(self, rhs: Any) -> None: + self.parent.mutate(self, VariableMutationType.ASSIGN, rhs) + + def add(self, other: Any) -> LHExpression: + return LHExpression(self, VariableMutationType.ADD, other) + + def subtract(self, other: Any) -> LHExpression: + return LHExpression(self, VariableMutationType.SUBTRACT, other) + + def multiply(self, other: Any) -> LHExpression: + return LHExpression(self, VariableMutationType.MULTIPLY, other) + + def divide(self, other: Any) -> LHExpression: + return LHExpression(self, VariableMutationType.DIVIDE, other) + + def extend(self, other: Any) -> LHExpression: + return LHExpression(self, VariableMutationType.EXTEND, other) + + def remove_if_present(self, other: Any) -> LHExpression: + return LHExpression(self, VariableMutationType.REMOVE_IF_PRESENT, other) + + def remove_index(self, index: Optional[Union[int, Any]] = None) -> LHExpression: + if index is None: + raise ValueError("Expected 'index' to be set, but it was None.") + return LHExpression(self, VariableMutationType.REMOVE_INDEX, index) + + def remove_key(self, key: Any) -> LHExpression: + return LHExpression(self, VariableMutationType.REMOVE_KEY, key) + def __str__(self) -> str: return to_json(self.compile()) @@ -1129,6 +1275,50 @@ def execute( node_name = self.add_node(readable_name, task_node) return NodeOutput(node_name) + def multiply(self, lhs: Any, rhs: Any) -> LHExpression: + return LHExpression(lhs, VariableMutationType.MULTIPLY, rhs) + + def add(self, lhs: Any, rhs: Any) -> LHExpression: + return LHExpression(lhs, VariableMutationType.ADD, rhs) + + def divide(self, lhs: Any, rhs: Any) -> LHExpression: + return LHExpression(lhs, VariableMutationType.DIVIDE, rhs) + + def subtract(self, lhs: Any, rhs: Any) -> LHExpression: + return LHExpression(lhs, VariableMutationType.SUBTRACT, rhs) + + def extend(self, lhs: Any, rhs: Any) -> LHExpression: + return LHExpression(lhs, VariableMutationType.EXTEND, rhs) + + def remove_if_present(self, lhs: Any, rhs: Any) -> LHExpression: + return LHExpression(lhs, VariableMutationType.REMOVE_IF_PRESENT, rhs) + + def remove_index(self, index: Optional[Union[int, Any]] = None) -> LHExpression: + if index is None: + raise ValueError("Expected 'index' to be set, but it was None.") + return LHExpression(self, VariableMutationType.REMOVE_INDEX, index) + + def remove_key(self, lhs: Any, rhs: Any) -> LHExpression: + return LHExpression(lhs, VariableMutationType.REMOVE_KEY, rhs) + + def declare_bool(self, name: str) -> WfRunVariable: + return self.add_variable(name, VariableType.BOOL) + + def declare_int(self, name: str) -> WfRunVariable: + return self.add_variable(name, VariableType.INT) + + def declare_double(self, name: str) -> WfRunVariable: + return self.add_variable(name, VariableType.DOUBLE) + + def declare_bytes(self, name: str) -> WfRunVariable: + return self.add_variable(name, VariableType.BYTES) + + def declare_json_arr(self, name: str) -> WfRunVariable: + return self.add_variable(name, VariableType.JSON_ARR) + + def declare_json_obj(self, name: str) -> WfRunVariable: + return self.add_variable(name, VariableType.JSON_OBJ) + def handle_any_failure( self, node: NodeOutput, initializer: "ThreadInitializer" ) -> None: @@ -1458,22 +1648,9 @@ def mutate( use the output of a Node Run to mutate variables). """ self._check_if_active() - last_node = self._last_node() - node_output: Optional[VariableMutation.NodeOutputSource] = None - rhs_assignment: Optional[VariableAssignment] = None literal_value: Optional[VariableValue] = None - - if isinstance(right_hand, NodeOutput): - if last_node.name != right_hand.node_name: - raise ReferenceError("NodeOutput does not match with last node") - node_output = VariableMutation.NodeOutputSource( - jsonpath=right_hand.json_path - ) - elif isinstance(right_hand, WfRunVariable): - rhs_assignment = to_variable_assignment(right_hand) - else: - literal_value = to_variable_value(right_hand) + rhs_assignment = to_variable_assignment(right_hand) mutation = VariableMutation( lhs_name=left_hand.name, @@ -1524,7 +1701,7 @@ def add_variable( raise ValueError(f"Variable {variable_name} already added") new_var = WfRunVariable( - variable_name, variable_type, default_value, access_level + variable_name, variable_type, self, default_value, access_level ) self._wf_run_variables.append(new_var) return new_var @@ -1934,7 +2111,6 @@ def with_task_timeout_seconds( self._default_timeout_seconds = timeout_seconds return self - def create_workflow_spec( workflow: Workflow, config: LHConfig, timeout: Optional[int] = None ) -> None: diff --git a/sdk-python/tests/test_utils.py b/sdk-python/tests/test_utils.py index 49d7aa511..9953eca08 100644 --- a/sdk-python/tests/test_utils.py +++ b/sdk-python/tests/test_utils.py @@ -228,7 +228,7 @@ def test_parse_assignment_variable(self): # a WfRunVariable wf_run_variable = WfRunVariable( - variable_name="my-var-name", variable_type=VariableType.STR + variable_name="my-var-name", variable_type=VariableType.STR, parent=None ) wf_run_variable.json_path = "$.myPath" variable = to_variable_assignment(wf_run_variable) diff --git a/sdk-python/tests/test_workflow.py b/sdk-python/tests/test_workflow.py index 6f3c1c59c..1604efd22 100644 --- a/sdk-python/tests/test_workflow.py +++ b/sdk-python/tests/test_workflow.py @@ -74,23 +74,23 @@ def test_validate_json_path_format(self): class TestWfRunVariable(unittest.TestCase): def test_value_is_not_none(self): - variable = WfRunVariable("my-var", VariableType.STR, default_value="my-str") + variable = WfRunVariable("my-var", VariableType.STR, None, default_value="my-str") self.assertEqual(variable.default_value.WhichOneof("value"), "str") self.assertEqual(variable.default_value.str, "my-str") - variable = WfRunVariable("my-var", VariableType.STR) + variable = WfRunVariable("my-var", VariableType.STR, None) self.assertEqual(variable.default_value, None) def test_validate_are_same_type(self): with self.assertRaises(TypeError) as exception_context: - WfRunVariable("my-var", VariableType.STR, 10) + WfRunVariable("my-var", VariableType.STR, None, 10) self.assertEqual( - "Default value is not a STR", + "Default value type does not match LH variable type STR", str(exception_context.exception), ) def test_validate_with_json_path_already_set(self): - variable = WfRunVariable("my-var", VariableType.STR) + variable = WfRunVariable("my-var", VariableType.STR, None) variable.json_path = "$.myPath" with self.assertRaises(ValueError) as exception_context: variable.with_json_path("$.myNewOne") @@ -100,7 +100,7 @@ def test_validate_with_json_path_already_set(self): ) def test_validate_json_path_already_set(self): - variable = WfRunVariable("my-var", VariableType.STR) + variable = WfRunVariable("my-var", VariableType.STR, None) variable.json_path = "$.myPath" with self.assertRaises(ValueError) as exception_context: variable.json_path = "$.myNewOne" @@ -110,7 +110,7 @@ def test_validate_json_path_already_set(self): ) def test_validate_json_path_format(self): - variable = WfRunVariable("my-var", VariableType.STR) + variable = WfRunVariable("my-var", VariableType.STR, None) with self.assertRaises(ValueError) as exception_context: variable.json_path = "$myNewOne" self.assertEqual( @@ -119,7 +119,7 @@ def test_validate_json_path_format(self): ) def test_validate_is_json_obj_when_using_json_index(self): - variable = WfRunVariable("my-var", VariableType.STR) + variable = WfRunVariable("my-var", VariableType.STR, None) with self.assertRaises(ValueError) as exception_context: variable.searchable_on("$.myPath", VariableType.STR) self.assertEqual( @@ -128,11 +128,11 @@ def test_validate_is_json_obj_when_using_json_index(self): ) def test_persistent(self): - variable = WfRunVariable("my-var", VariableType.STR).searchable() + variable = WfRunVariable("my-var", VariableType.STR, None).searchable() self.assertEqual(variable.compile().searchable, True) def test_validate_is_json_obj_when_using_json_pth(self): - variable = WfRunVariable("my-var", VariableType.STR) + variable = WfRunVariable("my-var", VariableType.STR, None) with self.assertRaises(ValueError) as exception_context: variable.with_json_path("$.myPath") self.assertEqual( @@ -140,29 +140,29 @@ def test_validate_is_json_obj_when_using_json_pth(self): str(exception_context.exception), ) - variable = WfRunVariable("my-var", VariableType.JSON_OBJ) + variable = WfRunVariable("my-var", VariableType.JSON_OBJ, None) variable.with_json_path("$.myPath") - variable = WfRunVariable("my-var", VariableType.JSON_ARR) + variable = WfRunVariable("my-var", VariableType.JSON_ARR, None) variable.with_json_path("$.myPath") def test_json_path_creates_new(self): - variable = WfRunVariable("my-var", VariableType.JSON_ARR) + variable = WfRunVariable("my-var", VariableType.JSON_ARR, None) with_json = variable.with_json_path("$.myPath") self.assertIsNot(variable, with_json) def test_compile_variable(self): - variable = WfRunVariable("my-var", VariableType.STR) + variable = WfRunVariable("my-var", VariableType.STR, None) self.assertEqual( variable.compile(), - ThreadVarDef(var_def=VariableDef(name="my-var", type=VariableType.STR)), + ThreadVarDef(var_def=VariableDef(name="my-var", type=VariableType.STR), access_level=WfRunVariableAccessLevel.PRIVATE_VAR), ) - variable = WfRunVariable("my-var", VariableType.JSON_OBJ) + variable = WfRunVariable("my-var", VariableType.JSON_OBJ, None) variable.searchable_on("$.myPath", VariableType.STR) expected_output = ThreadVarDef( var_def=VariableDef(name="my-var", type=VariableType.JSON_OBJ), - access_level="PUBLIC_VAR", + access_level="PRIVATE_VAR", ) expected_output.json_indexes.append( JsonIndex(field_path="$.myPath", field_type=VariableType.STR) @@ -170,7 +170,7 @@ def test_compile_variable(self): self.assertEqual(variable.compile(), expected_output) def test_compile_private_variable(self): - variable = WfRunVariable("my-var", VariableType.STR, access_level="PRIVATE_VAR") + variable = WfRunVariable("my-var", VariableType.STR, None, access_level="PRIVATE_VAR") expected_output = ThreadVarDef( var_def=VariableDef(name="my-var", type=VariableType.STR), access_level="PRIVATE_VAR", @@ -178,7 +178,7 @@ def test_compile_private_variable(self): self.assertEqual(variable.compile(), expected_output) def test_compile_inherited_variable(self): - variable = WfRunVariable("my-var", VariableType.STR) + variable = WfRunVariable("my-var", VariableType.STR, None) variable.with_access_level(WfRunVariableAccessLevel.INHERITED_VAR) expected_output = ThreadVarDef( var_def=VariableDef(name="my-var", type=VariableType.STR), @@ -286,7 +286,7 @@ class MyClass: def if_condition(self, thread: WorkflowThread) -> None: thread.mutate( WfRunVariable( - variable_name="variable-1", variable_type=VariableType.INT + variable_name="variable-1", variable_type=VariableType.INT, parent=thread ), VariableMutationType.ASSIGN, 1, @@ -294,7 +294,7 @@ def if_condition(self, thread: WorkflowThread) -> None: thread.execute("task-a") thread.mutate( WfRunVariable( - variable_name="variable-3", variable_type=VariableType.INT + variable_name="variable-3", variable_type=VariableType.INT, parent=thread ), VariableMutationType.ASSIGN, 3, @@ -304,7 +304,7 @@ def if_condition(self, thread: WorkflowThread) -> None: def else_condition(self, thread: WorkflowThread) -> None: thread.mutate( WfRunVariable( - variable_name="variable-2", variable_type=VariableType.INT + variable_name="variable-2", variable_type=VariableType.INT, parent=thread ), VariableMutationType.ASSIGN, 2, @@ -313,7 +313,7 @@ def else_condition(self, thread: WorkflowThread) -> None: thread.execute("task-d") thread.mutate( WfRunVariable( - variable_name="variable-4", variable_type=VariableType.INT + variable_name="variable-4", variable_type=VariableType.INT, parent=thread ), VariableMutationType.ASSIGN, 4, @@ -359,7 +359,7 @@ def to_thread(self): VariableMutation( lhs_name="variable-1", operation=VariableMutationType.ASSIGN, - literal_value=VariableValue(int=1), + rhs_assignment=VariableAssignment(literal_value=VariableValue(int=1)), ) ], ), @@ -378,7 +378,7 @@ def to_thread(self): VariableMutation( lhs_name="variable-2", operation=VariableMutationType.ASSIGN, - literal_value=VariableValue(int=2), + rhs_assignment=VariableAssignment(literal_value=VariableValue(int=2)), ) ], ), @@ -393,7 +393,7 @@ def to_thread(self): VariableMutation( lhs_name="variable-3", operation=VariableMutationType.ASSIGN, - literal_value=VariableValue(int=3), + rhs_assignment=VariableAssignment(literal_value=VariableValue(int=3)), ) ], ) @@ -416,7 +416,7 @@ def to_thread(self): VariableMutation( lhs_name="variable-4", operation=VariableMutationType.ASSIGN, - literal_value=VariableValue(int=4), + rhs_assignment=VariableAssignment(literal_value=VariableValue(int=4)), ) ], ) @@ -436,7 +436,7 @@ class MyClass: def if_condition(self, thread: WorkflowThread) -> None: thread.mutate( WfRunVariable( - variable_name="variable-1", variable_type=VariableType.INT + variable_name="variable-1", variable_type=VariableType.INT, parent=thread ), VariableMutationType.ASSIGN, 1, @@ -445,7 +445,7 @@ def if_condition(self, thread: WorkflowThread) -> None: def else_condition(self, thread: WorkflowThread) -> None: thread.mutate( WfRunVariable( - variable_name="variable-2", variable_type=VariableType.INT + variable_name="variable-2", variable_type=VariableType.INT, parent=thread ), VariableMutationType.ASSIGN, 2, @@ -491,7 +491,7 @@ def to_thread(self): VariableMutation( lhs_name="variable-2", operation=VariableMutationType.ASSIGN, - literal_value=VariableValue(int=2), + rhs_assignment=VariableAssignment(literal_value=VariableValue(int=2)), ) ], ), @@ -510,7 +510,7 @@ def to_thread(self): VariableMutation( lhs_name="variable-1", operation=VariableMutationType.ASSIGN, - literal_value=VariableValue(int=1), + rhs_assignment=VariableAssignment(literal_value=VariableValue(int=1)), ) ], ), @@ -530,7 +530,7 @@ class MyClass: def if_condition(self, thread: WorkflowThread) -> None: thread.mutate( WfRunVariable( - variable_name="variable-2", variable_type=VariableType.INT + variable_name="variable-2", variable_type=VariableType.INT, parent=thread ), VariableMutationType.ASSIGN, 2, @@ -539,7 +539,7 @@ def if_condition(self, thread: WorkflowThread) -> None: def my_entrypoint(self, thread: WorkflowThread) -> None: thread.mutate( WfRunVariable( - variable_name="variable-1", variable_type=VariableType.INT + variable_name="variable-1", variable_type=VariableType.INT, parent=thread ), VariableMutationType.ASSIGN, 1, @@ -549,7 +549,7 @@ def my_entrypoint(self, thread: WorkflowThread) -> None: ) thread.mutate( WfRunVariable( - variable_name="variable-3", variable_type=VariableType.INT + variable_name="variable-3", variable_type=VariableType.INT, parent=thread ), VariableMutationType.ASSIGN, 3, @@ -575,7 +575,7 @@ def to_thread(self): VariableMutation( lhs_name="variable-1", operation=VariableMutationType.ASSIGN, - literal_value=VariableValue(int=1), + rhs_assignment=VariableAssignment(literal_value=VariableValue(int=1)), ) ], ) @@ -599,7 +599,7 @@ def to_thread(self): VariableMutation( lhs_name="variable-2", operation=VariableMutationType.ASSIGN, - literal_value=VariableValue(int=2), + rhs_assignment=VariableAssignment(literal_value=VariableValue(int=2)), ) ], ), @@ -626,7 +626,7 @@ def to_thread(self): VariableMutation( lhs_name="variable-3", operation=VariableMutationType.ASSIGN, - literal_value=VariableValue(int=3), + rhs_assignment=VariableAssignment(literal_value=VariableValue(int=3)), ) ], ) @@ -642,7 +642,7 @@ class MyClass: def my_condition(self, thread: WorkflowThread) -> None: thread.mutate( WfRunVariable( - variable_name="variable-1", variable_type=VariableType.INT + variable_name="variable-1", variable_type=VariableType.INT, parent=thread ), VariableMutationType.ASSIGN, 1, @@ -650,7 +650,7 @@ def my_condition(self, thread: WorkflowThread) -> None: thread.execute("my-task") thread.mutate( WfRunVariable( - variable_name="variable-2", variable_type=VariableType.INT + variable_name="variable-2", variable_type=VariableType.INT, parent=thread ), VariableMutationType.ASSIGN, 2, @@ -694,7 +694,7 @@ def to_thread(self): VariableMutation( lhs_name="variable-1", operation=VariableMutationType.ASSIGN, - literal_value=VariableValue(int=1), + rhs_assignment=VariableAssignment(literal_value=VariableValue(int=1)), ) ], ), @@ -721,7 +721,7 @@ def to_thread(self): VariableMutation( lhs_name="variable-2", operation=VariableMutationType.ASSIGN, - literal_value=VariableValue(int=2), + rhs_assignment=VariableAssignment(literal_value=VariableValue(int=2)), ) ], ) @@ -741,7 +741,7 @@ class MyClass: def my_condition(self, thread: WorkflowThread) -> None: thread.mutate( WfRunVariable( - variable_name="variable-1", variable_type=VariableType.INT + variable_name="variable-1", variable_type=VariableType.INT, parent=thread ), VariableMutationType.ASSIGN, 1, @@ -749,7 +749,7 @@ def my_condition(self, thread: WorkflowThread) -> None: thread.execute("my-task") thread.mutate( WfRunVariable( - variable_name="variable-2", variable_type=VariableType.INT + variable_name="variable-2", variable_type=VariableType.INT, parent=thread ), VariableMutationType.ASSIGN, 2, @@ -793,7 +793,7 @@ def to_thread(self): VariableMutation( lhs_name="variable-1", operation=VariableMutationType.ASSIGN, - literal_value=VariableValue(int=1), + rhs_assignment=VariableAssignment(literal_value=VariableValue(int=1)), ) ], ), @@ -820,7 +820,7 @@ def to_thread(self): VariableMutation( lhs_name="variable-2", operation=VariableMutationType.ASSIGN, - literal_value=VariableValue(int=2), + rhs_assignment=VariableAssignment(literal_value=VariableValue(int=2)), ) ], ) @@ -1061,7 +1061,7 @@ def my_entrypoint(thread: WorkflowThread) -> None: VariableMutation( lhs_name="value", operation=VariableMutationType.MULTIPLY, - literal_value=VariableValue(int=2), + rhs_assignment=VariableAssignment(literal_value=VariableValue(int=2)), ) ], ) @@ -1079,6 +1079,76 @@ def my_entrypoint(thread: WorkflowThread) -> None: ), ) + def test_mutations_should_use_variable_assignments(self): + def my_entrypoint(thread: WorkflowThread) -> None: + my_var = thread.add_variable("my-var", VariableType.STR) + my_var.assign("some-value") + + wf_spec = Workflow("obiwan", my_entrypoint).compile() + entrypoint = wf_spec.thread_specs[wf_spec.entrypoint_thread_name] + node = entrypoint.nodes["0-entrypoint-ENTRYPOINT"] + + edge = node.outgoing_edges[0] + + self.assertEqual(edge.variable_mutations[0].rhs_assignment.literal_value.str, "some-value") + + def test_node_output_mutations_should_also_use_variable_assignments(self): + def my_entrypoint(thread: WorkflowThread) -> None: + my_var = thread.add_variable("my-var", VariableType.STR) + my_var.assign(thread.execute("use-the-force")) + + wf_spec = Workflow("obiwan", my_entrypoint).compile() + entrypoint = wf_spec.thread_specs[wf_spec.entrypoint_thread_name] + node = entrypoint.nodes["1-use-the-force-TASK"] + + edge = node.outgoing_edges[0] + + self.assertEqual(edge.variable_mutations[0].rhs_assignment.node_output.node_name, "1-use-the-force-TASK") + + def test_node_output_mutations_should_carry_json_path(self): + def my_entrypoint(thread: WorkflowThread) -> None: + my_var = thread.add_variable("my-var", VariableType.STR) + my_var.assign(thread.execute("use-the-force").with_json_path("$.hello.there")) + + wfSpec = Workflow("obiwan", my_entrypoint).compile() + entrypoint = wfSpec.thread_specs[wfSpec.entrypoint_thread_name] + node = entrypoint.nodes["1-use-the-force-TASK"] + + edge = node.outgoing_edges[0] + + self.assertEqual(edge.variable_mutations[0].rhs_assignment.node_output.node_name, "1-use-the-force-TASK") + + self.assertEqual(edge.variable_mutations[0].rhs_assignment.json_path, "$.hello.there") + + def test_assigning_variables_to_other_variables_should_use_variable_assignment(self): + def my_entrypoint(thread: WorkflowThread) -> None: + my_var = thread.add_variable("my-var", VariableType.STR) + other_var = thread.add_variable("other-var", VariableType.STR) + my_var.assign(other_var) + + wfSpec = Workflow("obiwan", my_entrypoint).compile() + entrypoint = wfSpec.thread_specs[wfSpec.entrypoint_thread_name] + node = entrypoint.nodes["0-entrypoint-ENTRYPOINT"] + + edge = node.outgoing_edges[0] + self.assertEqual(edge.variable_mutations[0].rhs_assignment.variable_name, "other-var") + + def test_assigning_variables_to_other_variables_should_carry_json_path(self): + def my_entrypoint(thread: WorkflowThread) -> None: + my_var = thread.add_variable("my-var", VariableType.STR) + other_var = thread.add_variable("other-var", VariableType.JSON_OBJ) + my_var.assign(other_var.with_json_path("$.hello.there")) + + wfSpec = Workflow("obiwan", my_entrypoint).compile() + entrypoint = wfSpec.thread_specs[wfSpec.entrypoint_thread_name] + node = entrypoint.nodes["0-entrypoint-ENTRYPOINT"] + + edge = node.outgoing_edges[0] + self.assertEqual(edge.variable_mutations[0].rhs_assignment.variable_name, "other-var") + + self.assertEqual(edge.variable_mutations[0].rhs_assignment.json_path, "$.hello.there") + + class TestWorkflow(unittest.TestCase): def test_entrypoint_is_a_function(self): @@ -2006,7 +2076,7 @@ def wf_func(thread: WorkflowThread) -> None: mutation = mutations[0] self.assertEqual("my-var", mutation.lhs_name) self.assertEqual(VariableMutationType.ASSIGN, mutation.operation) - self.assertTrue(mutation.HasField("node_output")) + self.assertTrue(mutation.HasField("rhs_assignment")) def test_assign_to_variable_user_id(self): def wf_func(thread: WorkflowThread) -> None: @@ -2171,7 +2241,7 @@ def wf_func(thread: WorkflowThread) -> None: def test_reassign_to_user_var(self): def wf_func(thread: WorkflowThread) -> None: - user_var = WfRunVariable("my-var", VariableType.STR) + user_var = WfRunVariable("my-var", VariableType.STR, thread) uto = thread.assign_user_task( "my-user-task", user_id="asdf", @@ -2197,7 +2267,7 @@ def wf_func(thread: WorkflowThread) -> None: def test_reassign_to_group(self): def wf_func(thread: WorkflowThread) -> None: - user_var = WfRunVariable("my-var", VariableType.STR) + user_var = WfRunVariable("my-var", VariableType.STR, thread) uto = thread.assign_user_task( "my-user-task", user_id="asdf", diff --git a/server/src/main/java/io/littlehorse/common/model/getable/core/init/ServerVersionModel.java b/server/src/main/java/io/littlehorse/common/model/getable/core/init/ServerVersionModel.java index 91e9bf37b..02229ae76 100644 --- a/server/src/main/java/io/littlehorse/common/model/getable/core/init/ServerVersionModel.java +++ b/server/src/main/java/io/littlehorse/common/model/getable/core/init/ServerVersionModel.java @@ -53,6 +53,8 @@ public void initFrom(Message proto, ExecutionContext context) throws LHSerdeErro if (serverVersion.hasPreReleaseIdentifier()) { this.preReleaseIdentifier = Optional.of(serverVersion.getPreReleaseIdentifier()); + } else { + this.preReleaseIdentifier = Optional.empty(); } } diff --git a/server/src/main/java/io/littlehorse/common/model/getable/core/taskrun/UserTaskTriggerReferenceModel.java b/server/src/main/java/io/littlehorse/common/model/getable/core/taskrun/UserTaskTriggerReferenceModel.java index c2fb65e23..bfbc19448 100644 --- a/server/src/main/java/io/littlehorse/common/model/getable/core/taskrun/UserTaskTriggerReferenceModel.java +++ b/server/src/main/java/io/littlehorse/common/model/getable/core/taskrun/UserTaskTriggerReferenceModel.java @@ -44,6 +44,14 @@ public UserTaskTriggerReference.Builder toProto() { .setNodeRunId(nodeRunId.toProto()) .setUserTaskEventNumber(userTaskEventNumber); + if (userId != null) { + out.setUserId(this.userId); + } + + if (userGroup != null) { + out.setUserGroup(this.userGroup); + } + return out; } @@ -52,6 +60,14 @@ public void initFrom(Message proto, ExecutionContext context) { UserTaskTriggerReference p = (UserTaskTriggerReference) proto; nodeRunId = LHSerializable.fromProto(p.getNodeRunId(), NodeRunIdModel.class, context); userTaskEventNumber = p.getUserTaskEventNumber(); + + if (p.hasUserId()) { + userId = p.getUserId(); + } + + if (p.hasUserGroup()) { + userGroup = p.getUserGroup(); + } } @Override diff --git a/server/src/test/java/e2e/BasicTest.java b/server/src/test/java/e2e/BasicTest.java index 094d1ab96..6e55ff439 100644 --- a/server/src/test/java/e2e/BasicTest.java +++ b/server/src/test/java/e2e/BasicTest.java @@ -27,7 +27,7 @@ public void shouldDoBasic() { public Workflow getBasic() { return new WorkflowImpl("test-basic", thread -> { WfRunVariable asdf = thread.declareBool("asdf"); - asdf.assignTo(thread.execute("ag-one")); + asdf.assign(thread.execute("ag-one")); }); } diff --git a/server/src/test/java/e2e/ExpressionTest.java b/server/src/test/java/e2e/ExpressionTest.java index 13f5b5117..c9c4962a1 100644 --- a/server/src/test/java/e2e/ExpressionTest.java +++ b/server/src/test/java/e2e/ExpressionTest.java @@ -247,7 +247,7 @@ public Workflow getExpression() { // EXTEND a String test var myStr = wf.declareStr("my-str"); wf.doIf(myStr.isNotEqualTo(null), then -> { - myStr.assignTo(myStr.extend("-suffix")); + myStr.assign(myStr.extend("-suffix")); }); // Add an int and composite expressions @@ -255,14 +255,14 @@ public Workflow getExpression() { var intToAddResult = wf.declareInt("int-to-add-result"); wf.doIf(intToAdd.isNotEqualTo(null), then -> { // Tests compound expressions - intToAddResult.assignTo(wf.execute("expr-add-one", intToAdd.add(1))); + intToAddResult.assign(wf.execute("expr-add-one", intToAdd.add(1))); }); // Division By Zero test var thingToDivideByZero = wf.declareInt("thing-to-divide-by-zero"); var divideByZeroResult = wf.declareInt("divide-by-zero-result"); wf.doIf(thingToDivideByZero.isNotEqualTo(null), then -> { - divideByZeroResult.assignTo(thingToDivideByZero.divide(0)); + divideByZeroResult.assign(thingToDivideByZero.divide(0)); }); // Test precision of arithmetic. Make use of the fact that we don't have @@ -273,8 +273,8 @@ public Workflow getExpression() { var divisionResultInt = wf.declareInt("division-result-int"); wf.doIf(divisionTestJson.isNotEqualTo(null), then -> { LHExpression foobar = divisionTestJson.jsonPath("$.lhs").divide(divisionTestJson.jsonPath("$.rhs")); - divisionResult.assignTo(foobar); - divisionResultInt.assignTo(foobar); + divisionResult.assign(foobar); + divisionResultInt.assign(foobar); }); // This test uses a complex expression where the things we are computing over @@ -289,20 +289,20 @@ public Workflow getExpression() { // TotalPrice = Quantity * Price * (1 - DiscountPercentage / 100) LHExpression pedro = quantity.multiply(price).multiply(wf.subtract(1.0, discountPercentage.divide(100.0))); - totalPriceInt.assignTo(pedro); - totalPriceDouble.assignTo(pedro); + totalPriceInt.assign(pedro); + totalPriceDouble.assign(pedro); }); // Test mutating sub-fields of a json object var json = wf.declareJsonObj("json"); wf.doIf(json.isNotEqualTo(null), then -> { - json.jsonPath("$.foo").assignTo("bar"); + json.jsonPath("$.foo").assign("bar"); }); // Test mutating doubly-nested fields of a Json Object var nestedJson = wf.declareJsonObj("nested-json"); wf.doIf(nestedJson.isNotEqualTo(null), then -> { - nestedJson.jsonPath("$.foo.bar").assignTo("baz"); + nestedJson.jsonPath("$.foo.bar").assign("baz"); }); }); } diff --git a/server/src/test/java/e2e/UserTaskTest.java b/server/src/test/java/e2e/UserTaskTest.java index 773f6bbc8..4550ca55f 100644 --- a/server/src/test/java/e2e/UserTaskTest.java +++ b/server/src/test/java/e2e/UserTaskTest.java @@ -13,6 +13,9 @@ import io.littlehorse.sdk.common.proto.SaveUserTaskRunProgressRequest; import io.littlehorse.sdk.common.proto.SaveUserTaskRunProgressRequest.SaveUserTaskRunAssignmentPolicy; import io.littlehorse.sdk.common.proto.SearchWfRunRequest; +import io.littlehorse.sdk.common.proto.TaskRun; +import io.littlehorse.sdk.common.proto.TaskRunId; +import io.littlehorse.sdk.common.proto.TaskStatus; import io.littlehorse.sdk.common.proto.UserTaskEvent; import io.littlehorse.sdk.common.proto.UserTaskEvent.EventCase; import io.littlehorse.sdk.common.proto.UserTaskRun; @@ -62,6 +65,12 @@ public class UserTaskTest { @LHWorkflow("cancel-user-task-on-deadline") private Workflow userTaskCancelOnDeadline; + @LHWorkflow("schedule-reminder-task-without-user-fields-workflow") + private Workflow scheduleReminderTaskWithoutUserFields; + + @LHWorkflow("worker-context-receives-user-details") + private Workflow workerContextReceivesUserDetails; + @LHUserTaskForm(USER_TASK_DEF_NAME) private MyForm myForm = new MyForm(); @@ -296,6 +305,42 @@ void shouldExecuteBusinessExceptionHandlerWhenUserTaskGetsCancelOnDeadline() { .start(); } + @Test + void shouldScheduleAndExecuteReminderTask() { + workflowVerifier + .prepareRun(scheduleReminderTaskWithoutUserFields) + .waitForStatus(ERROR, Duration.ofSeconds(6)) + .thenVerifyNodeRun(0, 1, nodeRun -> { + UserTaskRunId userTaskId = nodeRun.getUserTask().getUserTaskRunId(); + UserTaskRun userTaskRun = client.getUserTaskRun(userTaskId); + UserTaskEvent userTaskEvent = userTaskRun.getEvents(1); + TaskRunId taskRunId = userTaskEvent.getTaskExecuted().getTaskRun(); + TaskRun taskRun = client.getTaskRun(taskRunId); + TaskStatus taskRunStatus = taskRun.getStatus(); + + Assertions.assertThat(taskRunStatus).isEqualTo(TaskStatus.TASK_SUCCESS); + }) + .start(); + } + + @Test + void verifyWorkerContextHasUserIdOrUserGroup() { + workflowVerifier + .prepareRun(workerContextReceivesUserDetails) + .waitForStatus(ERROR, Duration.ofSeconds(6)) + .thenVerifyNodeRun(0, 1, nodeRun -> { + UserTaskRunId userTaskId = nodeRun.getUserTask().getUserTaskRunId(); + UserTaskRun userTaskRun = client.getUserTaskRun(userTaskId); + UserTaskEvent userTaskEvent = userTaskRun.getEvents(1); + TaskRunId taskRunId = userTaskEvent.getTaskExecuted().getTaskRun(); + TaskRun taskRun = client.getTaskRun(taskRunId); + TaskStatus taskRunStatus = taskRun.getStatus(); + + Assertions.assertThat(taskRunStatus).isEqualTo(TaskStatus.TASK_SUCCESS); + }) + .start(); + } + @LHWorkflow("deadline-reassignment-workflow") public Workflow buildDeadlineReassignmentWorkflow() { return new WorkflowImpl("deadline-reassignment-workflow", entrypointThread -> { @@ -315,6 +360,30 @@ public Workflow buildDeadlineReassignmentWorkflow() { }); } + @LHWorkflow("schedule-reminder-task-without-user-fields-workflow") + public Workflow buildReminderTaskWorkflowWithUserGroupField() { + return new WorkflowImpl("reminder-task-without-user-fields-workflow", entrypointThread -> { + UserTaskOutput formOutput = entrypointThread.assignUserTask(USER_TASK_DEF_NAME, "jacob", null); + + // Schedule a reminder immediately + entrypointThread.scheduleReminderTask(formOutput, 0, "reminder-task"); + + entrypointThread.cancelUserTaskRunAfter(formOutput, 5); + }); + } + + @LHWorkflow("worker-context-receives-user-details") + public Workflow workerContextReceivesUserDetails() { + return new WorkflowImpl("worker-context-receives-user-details", entrypointThread -> { + UserTaskOutput formOutput = entrypointThread.assignUserTask(USER_TASK_DEF_NAME, "jacob", null); + + // Schedule a reminder immediately + entrypointThread.scheduleReminderTask(formOutput, 0, "verify-worker-context", "jacob", null); + + entrypointThread.cancelUserTaskRunAfter(formOutput, 5); + }); + } + @LHWorkflow("deadline-reassignment-workflow-user-without-group") public Workflow buildDeadlineReassignmentWorkflowUserWithoutGroup() { return new WorkflowImpl("deadline-reassignment-workflow-user-without-group", entrypointThread -> { @@ -369,6 +438,24 @@ public String userTaskCanceled() { public void doReminder(WorkerContext ctx) { cache.put(ctx.getWfRunId().getId(), "hello there!"); } + + @LHTaskMethod("verify-worker-context") + public void verifyWorkerContext(String userId, String userGroup, WorkerContext ctx) { + if (userId == null && userGroup == null) { + throw new IllegalStateException("At least one of userId or userGroup must be specified"); + } + + if (userId != null) { + if (!userId.equals(ctx.getUserId())) { + throw new IllegalStateException("WorkerContext UserId does not match expected value."); + } + } + if (userGroup != null) { + if (!userGroup.equals(ctx.getUserGroup())) { + throw new IllegalStateException("WorkerContext UserGroup does not match expected value."); + } + } + } } class MyForm {