diff --git a/.github/workflows/ui-next-ci.yml b/.github/workflows/ui-next-ci.yml new file mode 100644 index 0000000000..c9b15ad67f --- /dev/null +++ b/.github/workflows/ui-next-ci.yml @@ -0,0 +1,61 @@ +name: UI v2 CI + +on: + pull_request: + branches: + - main + paths: + - "ui-next/**" + +permissions: + contents: read + +jobs: + lint-format-test: + name: Lint, Format & Test + runs-on: ubuntu-latest + defaults: + run: + working-directory: ui-next + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.32.0 + + - name: Get pnpm store directory + id: pnpm-cache + run: echo "store=$(pnpm store path)" >> $GITHUB_OUTPUT + + - name: Cache pnpm store + uses: actions/cache@v4 + with: + path: ${{ steps.pnpm-cache.outputs.store }} + key: ${{ runner.os }}-pnpm-${{ hashFiles('ui-next/pnpm-lock.yaml') }} + restore-keys: ${{ runner.os }}-pnpm- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Prettier check + run: pnpm prettier:check + + - name: Lint + run: pnpm lint + + - name: Type check + run: pnpm typecheck + + - name: Test + run: pnpm test + + - name: Build + run: pnpm build diff --git a/ui-next/.env b/ui-next/.env new file mode 100644 index 0000000000..4e5b3adcb8 --- /dev/null +++ b/ui-next/.env @@ -0,0 +1,8 @@ +# OSS Conductor UI – defaults for local dev + +# Backend API (Conductor server). Default: local server. +VITE_WF_SERVER=http://localhost:8080 + +# Optional +# VITE_PUBLIC_URL=/ +# GENERATE_SOURCEMAP=false diff --git a/ui-next/.gitignore b/ui-next/.gitignore new file mode 100644 index 0000000000..7d4dfc8a78 --- /dev/null +++ b/ui-next/.gitignore @@ -0,0 +1,26 @@ +# Dependencies +node_modules + +# Local env (may contain secrets) +.env.local +.env.*.local + +# Build output +dist +build +storybook-static + +# Test / Playwright +/test-results/ +/playwright-report/ +/playwright/.cache/ + +# Turbo +.turbo + +# Vite +.vite + +# IDE / OS +.DS_Store + diff --git a/ui-next/.npmrc b/ui-next/.npmrc new file mode 100644 index 0000000000..5406fe67db --- /dev/null +++ b/ui-next/.npmrc @@ -0,0 +1,7 @@ +# Hoist these packages so they are directly importable without needing explicit +# devDependency declarations for each transitive package. +public-hoist-pattern[]=@mui/* +public-hoist-pattern[]=@use-gesture/* +public-hoist-pattern[]=@eslint/* +public-hoist-pattern[]=globals +public-hoist-pattern[]=monaco-editor \ No newline at end of file diff --git a/ui-next/.prettierignore b/ui-next/.prettierignore new file mode 100644 index 0000000000..08437e2d75 --- /dev/null +++ b/ui-next/.prettierignore @@ -0,0 +1,10 @@ +build +storybook-static +/test-results/ +/playwright-report/ +/playwright/.cache/ +tests-examples +playwright +public/context.js +/dist/ +pnpm-lock.yaml diff --git a/ui-next/.prettierrc.json b/ui-next/.prettierrc.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/ui-next/.prettierrc.json @@ -0,0 +1 @@ +{} diff --git a/ui-next/README.md b/ui-next/README.md new file mode 100644 index 0000000000..1ec124ae73 --- /dev/null +++ b/ui-next/README.md @@ -0,0 +1,171 @@ +# Conductor UI v2 + +The open-source React UI for [Conductor](https://github.com/conductor-oss/conductor). It ships as both a **standalone web application** and an **npm library** that enterprise packages can extend via a plugin system. + +## Running locally + +### Prerequisites + +- Node.js 22+ +- [pnpm](https://pnpm.io/) 10.32.0 (`corepack use pnpm@10.32.0`) +- A running Conductor server (default: `http://localhost:8080`) + +### Setup + +```bash +pnpm install +``` + +Configure the backend URL in `.env` (see `.env` for defaults): + +```bash +VITE_WF_SERVER=http://localhost:8080 +``` + +### Start the dev server + +```bash +pnpm dev +``` + +The app will be available at `http://localhost:1234`. + +### Runtime configuration + +The app reads runtime config from `public/context.js`, which is loaded at startup (not bundled). Copy the example and edit as needed: + +```bash +cp public/context.js.example public/context.js +``` + +This file sets feature flags (`window.conductor`) and auth config (`window.authConfig`) without requiring a rebuild. + +## Available scripts + +| Script | Description | +| --------------------- | ------------------------------- | +| `pnpm dev` | Start dev server with HMR | +| `pnpm build` | Build standalone app to `dist/` | +| `pnpm build:lib` | Build npm library to `dist/` | +| `pnpm build:all` | Build both app and library | +| `pnpm lint` | Run ESLint | +| `pnpm lint:fix` | Run ESLint with auto-fix | +| `pnpm prettier:check` | Check formatting | +| `pnpm prettier:write` | Auto-format all files | +| `pnpm typecheck` | Type-check without emitting | +| `pnpm test` | Run unit tests | +| `pnpm test:watch` | Run tests in watch mode | +| `pnpm test:coverage` | Run tests with coverage report | + +## Using as an npm library + +Install the package: + +```bash +npm install conductor-ui +``` + +Import styles in your app entry point: + +```tsx +import "conductor-ui/styles.css"; // component styles +import "conductor-ui/global.css"; // global body/font styles (optional) +``` + +### Extending with plugins + +The plugin system lets you register additional routes, sidebar items, task forms, auth providers, and more without modifying the core package. + +```tsx +import { pluginRegistry, App } from "conductor-ui"; + +// Register a custom sidebar item +pluginRegistry.registerSidebarItem({ + position: { target: "root", after: "definitionsSubMenu" }, + item: { + id: "myFeature", + title: "My Feature", + icon: , + linkTo: "/my-feature", + shortcuts: [], + hidden: false, + position: 350, + }, +}); + +// Register a custom route +pluginRegistry.registerRoutes([ + { + path: "/my-feature", + element: , + }, +]); + +// Render the app +function Root() { + return ; +} +``` + +### Plugin extension points + +| Extension | Method | Description | +| --------------- | ------------------------------ | -------------------------------------------------- | +| Routes | `registerRoutes(routes)` | Add authenticated routes | +| Public routes | `registerPublicRoutes(routes)` | Add unauthenticated routes | +| Sidebar items | `registerSidebarItem(reg)` | Inject items into the sidebar | +| Task forms | `registerTaskForm(reg)` | Custom forms for task types in the workflow editor | +| Task menu items | `registerTaskMenuItem(reg)` | Add task types to the "Add Task" menu | +| Auth provider | `registerAuthProvider(reg)` | Replace the auth implementation | +| Search provider | `registerSearchProvider(reg)` | Add results to global search | + +### Sidebar item positioning + +Sidebar items use numeric positions so plugins can inject between core items without collisions. The core OSS positions are exported for reference: + +```tsx +import { CORE_SIDEBAR_POSITIONS } from "conductor-ui"; + +// CORE_SIDEBAR_POSITIONS.ROOT: +// executionsSubMenu: 100 +// runWorkflow: 200 +// definitionsSubMenu:300 +// helpMenu: 400 +// swaggerItem: 500 + +pluginRegistry.registerSidebarItem({ + position: { target: "root" }, + item: { + id: "myItem", + position: 350, // between definitionsSubMenu (300) and helpMenu (400) + // ... + }, +}); +``` + +## Project structure + +``` +src/ +├── components/ # Shared UI components +│ └── Sidebar/ # Sidebar with plugin-injectable menu +├── pages/ # Route-level page components +├── plugins/ # Plugin registry and fetch utilities +├── shared/ # Auth state machine and context +├── theme/ # MUI theme provider +├── types/ # Shared TypeScript types +└── utils/ # Feature flags, constants, helpers +public/ +├── context.js # Runtime config (gitignored, not bundled) +└── context.js.example +``` + +## Peer dependencies + +When consuming as a library, the following must be provided by the host app: + +- `react` ^18 +- `react-dom` ^18 +- `react-router` / `react-router-dom` ^7 +- `@mui/material`, `@mui/icons-material`, `@mui/system`, `@mui/x-date-pickers` +- `@emotion/react`, `@emotion/styled` diff --git a/ui-next/eslint.config.mjs b/ui-next/eslint.config.mjs new file mode 100644 index 0000000000..9606a2f09e --- /dev/null +++ b/ui-next/eslint.config.mjs @@ -0,0 +1,105 @@ +import eslintReact from "@eslint-react/eslint-plugin"; +import js from "@eslint/js"; +import vitest from "@vitest/eslint-plugin"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import { globalIgnores } from "eslint/config"; +import globals from "globals"; +import tseslint from "typescript-eslint"; + +const commonRules = { + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-unused-vars": [ + "error", + { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, + ], + // TODO: Remove this and fix types properly + "@typescript-eslint/ban-ts-comment": "warn", + // Prevent direct imports from date-fns and date-fns-tz except in utils/date.ts + "no-restricted-imports": [ + "error", + { + patterns: [ + { + group: ["date-fns"], + message: + "Direct imports from 'date-fns' are not allowed. Please import from 'src/utils/date' instead.", + }, + { + group: ["date-fns-tz"], + message: + "Direct imports from 'date-fns-tz' are not allowed. Please import from 'src/utils/date' instead.", + }, + ], + }, + ], +}; + +const baseConfig = { + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs["recommended-latest"], + reactRefresh.configs.vite, + eslintReact.configs.recommended, + ], + languageOptions: { + ecmaVersion: 2020, + sourceType: "module", + globals: { + ...globals.browser, + ...globals.node, + }, + }, + rules: { + "no-undef": "error", + ...commonRules, + }, +}; + +export default tseslint.config([ + globalIgnores(["dist", "node_modules"]), + + // Test files (Vitest + testing globals) + { + files: [ + "**/__tests__/**/*.{js,jsx,ts,tsx}", + "**/*.{test,spec}.{js,jsx,ts,tsx}", + ], + ...baseConfig, + plugins: { vitest, ...baseConfig.plugins }, + rules: { + ...vitest.configs.recommended.rules, + ...commonRules, + }, + }, + + // JSX files (allow PropTypes) + { + files: ["**/*.jsx"], + ...baseConfig, + rules: { + ...baseConfig.rules, + "react/prop-types": "off", + "@eslint-react/no-prop-types": "off", + }, + }, + + // Non-test files (TS/TSX) + { + files: ["**/*.{js,ts,tsx}"], + ignores: [ + "**/__tests__/**/*.{js,jsx,ts,tsx}", + "**/*.{test,spec}.{js,jsx,ts,tsx}", + ], + ...baseConfig, + }, + + // Allow date-fns and date-fns-tz imports in utils/date.ts + { + files: ["src/utils/date.ts"], + rules: { + "no-restricted-imports": "off", + }, + }, +]); diff --git a/ui-next/index.html b/ui-next/index.html new file mode 100644 index 0000000000..55c878eff6 --- /dev/null +++ b/ui-next/index.html @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Conductor UI + + + + +
+ + + + diff --git a/ui-next/package.json b/ui-next/package.json new file mode 100644 index 0000000000..beb0a28aea --- /dev/null +++ b/ui-next/package.json @@ -0,0 +1,174 @@ +{ + "name": "conductor-ui", + "version": "0.0.0", + "description": "Open Source Conductor UI - Core components, pages, and plugin infrastructure", + "type": "module", + "main": "./dist/conductor-ui.js", + "module": "./dist/conductor-ui.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/conductor-ui.js", + "types": "./dist/index.d.ts" + }, + "./styles.css": "./dist/style.css", + "./global.css": "./dist/global.css" + }, + "files": [ + "dist" + ], + "scripts": { + "dev": "vite", + "build": "vite build", + "build:lib": "vite build --mode lib && cp src/index.css dist/global.css", + "build:all": "vite build && vite build --mode lib", + "preview": "vite preview", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest --watch", + "test:coverage": "vitest run --coverage", + "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}' --quiet", + "lint:fix": "eslint 'src/**/*.{js,jsx,ts,tsx}' --fix", + "prettier:write": "prettier --write .", + "prettier:check": "prettier --check ." + }, + "peerDependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "dependencies": { + "@auth0/auth0-react": "^1.12.0", + "@datasert/cronjs-matcher": "^1.4.0", + "@dnd-kit/core": "^6.1.0", + "@dnd-kit/sortable": "^8.0.0", + "@emotion/react": "^11.11.1", + "@emotion/styled": "^11.10.8", + "@growthbook/growthbook": "^1.5.1", + "@growthbook/growthbook-react": "^1.5.1", + "@hookform/resolvers": "^5.2.1", + "@jsonforms/core": "^3.6.0", + "@jsonforms/material-renderers": "^3.6.0", + "@jsonforms/react": "^3.6.0", + "@monaco-editor/react": "^4.7.0", + "@mui/icons-material": "^7.3.1", + "@mui/material": "^7.3.1", + "@mui/system": "^7.3.1", + "@mui/x-date-pickers": "^6.16", + "@okta/okta-auth-js": "^6.9.0", + "@okta/okta-react": "^6.6.0", + "@okta/okta-signin-widget": "^6.9.0", + "@phosphor-icons/react": "^2.1.10", + "@use-gesture/react": "^10.2.21", + "@xstate/inspect": "^0.8.0", + "@xstate/react": "^3.2.1", + "ajv": "^8.17.1", + "ajv-errors": "^3.0.0", + "ajv-formats": "^3.0.1", + "autosuggest-highlight": "^3.3.4", + "classnames": "^2.3.1", + "color": "^4.2.3", + "cron-validate": "^1.4.3", + "cronstrue": "^2.32.0", + "date-fns": "^2.29.3", + "date-fns-tz": "^2.0.0", + "dom-to-image": "^2.6.0", + "fast-deep-equal": "^3.1.3", + "fuse.js": "^6.6.2", + "highlight.js": "^11.11.1", + "jotai": "^2.15.0", + "lodash": "^4.17.20", + "mock-json-schema": "^1.1.1", + "monaco-languages-jq": "^1.0.0", + "path-to-regexp": "^8.2.0", + "prismjs": "^1.27.0", + "prop-types": "^15.7.2", + "qs": "^6.14.0", + "react-confetti": "^6.4.0", + "react-container-query": "^0.12.0", + "react-data-table-component": "^7.5.3", + "react-datepicker": "^6.1.0", + "react-helmet": "^6.1.0", + "react-highlight": "^0.15.0", + "react-hook-form": "^7.62.0", + "react-hotkeys-hook": "^4.4.1", + "react-markdown": "10.1.0", + "react-number-format": "^5.3.1", + "react-player": "^2.16.0", + "react-query": "^3.39.2", + "react-router": "^7.12.0", + "react-router-dom": "^7.9.2", + "react-router-use-location-state": "^3.1.2", + "react-syntax-highlighter": "^15.5.0", + "react-vis-timeline": "^2.0.3", + "reaflow": "5.1.2", + "recharts": "^2.10.3", + "remark-directive": "^3.0.0", + "remark-gfm": "^4.0.0", + "styled-components": "^5.3.8", + "swagger-ui-react": "^5.29.4", + "ts-key-enum": "^2.0.12", + "unist-util-visit": "^5.0.0", + "url-parse": "^1.5.9", + "uuid": "^8.3.2", + "xstate": "^4.38.3", + "yup": "^1.7.0" + }, + "devDependencies": { + "@eslint-react/eslint-plugin": "^1.53.0", + "@io-orkes/conductor-javascript": "^2.1.4", + "@playwright/test": "^1.56.0", + "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "@types/autosuggest-highlight": "^3.2.0", + "@types/dom-to-image": "^2.6.7", + "@types/lodash": "^4.14.178", + "@types/node": "^24.10.1", + "@types/qs": "^6.14.0", + "@types/react": "^18.2.0", + "@types/react-beautiful-dnd": "^13.1.3", + "@types/react-datepicker": "^6.0.1", + "@types/react-dom": "^18.2.0", + "@types/react-helmet": "^6.1.5", + "@types/react-highlight": "^0.12.8", + "@types/react-syntax-highlighter": "^15.5.13", + "@types/swagger-ui-react": "^5.18.0", + "@types/url-parse": "^1.4.11", + "@types/uuid": "^8.3.4", + "@types/yup": "^0.32.0", + "@vitejs/plugin-react": "^4.6.0", + "@vitest/eslint-plugin": "^1.3.4", + "dotenv": "^16.4.5", + "eslint": "9.32.0", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-import": "^2.28.1", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "eslint-plugin-unused-imports": "^3.0.0", + "jsdom": "^26.1.0", + "monaco-editor": "^0.55.1", + "prettier": "^3.6.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "sass": "^1.50.0", + "typescript": "^5.9.2", + "typescript-eslint": "^8.35.1", + "vite": "^7.1.11", + "vite-plugin-dts": "^4.5.0", + "vite-plugin-svgr": "^4.3.0", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^3.2.4" + }, + "resolutions": { + "vis-timeline": "7.3.6", + "mini-css-extract-plugin": "2.4.5" + }, + "pnpm": { + "overrides": { + "vis-timeline": "7.3.6", + "mini-css-extract-plugin": "2.4.5" + } + }, + "packageManager": "pnpm@10.32.0+sha512.9b2634bb3fed5601c33633f2d92593f506270a3963b8c51d2b2d6a828da615ce4e9deebef9614ccebbc13ac8d3c0f9c9ccceb583c69c8578436fa477dbb20d70" +} diff --git a/ui-next/pnpm-lock.yaml b/ui-next/pnpm-lock.yaml new file mode 100644 index 0000000000..85ca453668 --- /dev/null +++ b/ui-next/pnpm-lock.yaml @@ -0,0 +1,11935 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + vis-timeline: 7.3.6 + mini-css-extract-plugin: 2.4.5 + +importers: + + .: + dependencies: + '@auth0/auth0-react': + specifier: ^1.12.0 + version: 1.12.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@datasert/cronjs-matcher': + specifier: ^1.4.0 + version: 1.4.0 + '@dnd-kit/core': + specifier: ^6.1.0 + version: 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@dnd-kit/sortable': + specifier: ^8.0.0 + version: 8.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + '@emotion/react': + specifier: ^11.11.1 + version: 11.14.0(@types/react@18.3.28)(react@18.3.1) + '@emotion/styled': + specifier: ^11.10.8 + version: 11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1) + '@growthbook/growthbook': + specifier: ^1.5.1 + version: 1.6.5 + '@growthbook/growthbook-react': + specifier: ^1.5.1 + version: 1.6.5(react@18.3.1) + '@hookform/resolvers': + specifier: ^5.2.1 + version: 5.2.2(react-hook-form@7.71.2(react@18.3.1)) + '@jsonforms/core': + specifier: ^3.6.0 + version: 3.7.0 + '@jsonforms/material-renderers': + specifier: ^3.6.0 + version: 3.7.0(9e4c19b25267976d34663ea61a4c9150) + '@jsonforms/react': + specifier: ^3.6.0 + version: 3.7.0(@jsonforms/core@3.7.0)(react@18.3.1) + '@monaco-editor/react': + specifier: ^4.7.0 + version: 4.7.0(monaco-editor@0.55.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@mui/icons-material': + specifier: ^7.3.1 + version: 7.3.9(@mui/material@7.3.9(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.28)(react@18.3.1) + '@mui/material': + specifier: ^7.3.1 + version: 7.3.9(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@mui/system': + specifier: ^7.3.1 + version: 7.3.9(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1) + '@mui/x-date-pickers': + specifier: ^6.16 + version: 6.20.2(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@mui/material@7.3.9(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@7.3.9(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(date-fns@2.30.0)(dayjs@1.10.7)(luxon@3.7.2)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@okta/okta-auth-js': + specifier: ^6.9.0 + version: 6.9.0 + '@okta/okta-react': + specifier: ^6.6.0 + version: 6.10.0(@okta/okta-auth-js@6.9.0)(react-dom@18.3.1(react@18.3.1))(react-router-dom@7.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + '@okta/okta-signin-widget': + specifier: ^6.9.0 + version: 6.9.0 + '@phosphor-icons/react': + specifier: ^2.1.10 + version: 2.1.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@use-gesture/react': + specifier: ^10.2.21 + version: 10.3.1(react@18.3.1) + '@xstate/inspect': + specifier: ^0.8.0 + version: 0.8.0(ws@8.19.0)(xstate@4.38.3) + '@xstate/react': + specifier: ^3.2.1 + version: 3.2.2(@types/react@18.3.28)(react@18.3.1)(xstate@4.38.3) + ajv: + specifier: ^8.17.1 + version: 8.18.0 + ajv-errors: + specifier: ^3.0.0 + version: 3.0.0(ajv@8.18.0) + ajv-formats: + specifier: ^3.0.1 + version: 3.0.1(ajv@8.18.0) + autosuggest-highlight: + specifier: ^3.3.4 + version: 3.3.4 + classnames: + specifier: ^2.3.1 + version: 2.5.1 + color: + specifier: ^4.2.3 + version: 4.2.3 + cron-validate: + specifier: ^1.4.3 + version: 1.5.3 + cronstrue: + specifier: ^2.32.0 + version: 2.61.0 + date-fns: + specifier: ^2.29.3 + version: 2.30.0 + date-fns-tz: + specifier: ^2.0.0 + version: 2.0.1(date-fns@2.30.0) + dom-to-image: + specifier: ^2.6.0 + version: 2.6.0 + fast-deep-equal: + specifier: ^3.1.3 + version: 3.1.3 + fuse.js: + specifier: ^6.6.2 + version: 6.6.2 + highlight.js: + specifier: ^11.11.1 + version: 11.11.1 + jotai: + specifier: ^2.15.0 + version: 2.18.1(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@18.3.28)(react@18.3.1) + lodash: + specifier: ^4.17.20 + version: 4.17.23 + mock-json-schema: + specifier: ^1.1.1 + version: 1.1.2 + monaco-languages-jq: + specifier: ^1.0.0 + version: 1.0.0 + path-to-regexp: + specifier: ^8.2.0 + version: 8.3.0 + prismjs: + specifier: ^1.27.0 + version: 1.30.0 + prop-types: + specifier: ^15.7.2 + version: 15.8.1 + qs: + specifier: ^6.14.0 + version: 6.15.0 + react-confetti: + specifier: ^6.4.0 + version: 6.4.0(react@18.3.1) + react-container-query: + specifier: ^0.12.0 + version: 0.12.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-data-table-component: + specifier: ^7.5.3 + version: 7.7.0(react@18.3.1)(styled-components@5.3.11(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react-is@19.2.4)(react@18.3.1)) + react-datepicker: + specifier: ^6.1.0 + version: 6.9.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-helmet: + specifier: ^6.1.0 + version: 6.1.0(react@18.3.1) + react-highlight: + specifier: ^0.15.0 + version: 0.15.0 + react-hook-form: + specifier: ^7.62.0 + version: 7.71.2(react@18.3.1) + react-hotkeys-hook: + specifier: ^4.4.1 + version: 4.6.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-markdown: + specifier: 10.1.0 + version: 10.1.0(@types/react@18.3.28)(react@18.3.1) + react-number-format: + specifier: ^5.3.1 + version: 5.4.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-player: + specifier: ^2.16.0 + version: 2.16.1(react@18.3.1) + react-query: + specifier: ^3.39.2 + version: 3.39.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-router: + specifier: ^7.12.0 + version: 7.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-router-dom: + specifier: ^7.9.2 + version: 7.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-router-use-location-state: + specifier: ^3.1.2 + version: 3.1.2(@types/react@18.3.28)(next@16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.97.3))(react-router@7.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + react-syntax-highlighter: + specifier: ^15.5.0 + version: 15.6.6(react@18.3.1) + react-vis-timeline: + specifier: ^2.0.3 + version: 2.0.3(lodash@4.17.23)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + reaflow: + specifier: 5.1.2 + version: 5.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + recharts: + specifier: ^2.10.3 + version: 2.15.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + remark-directive: + specifier: ^3.0.0 + version: 3.0.1 + remark-gfm: + specifier: ^4.0.0 + version: 4.0.1 + styled-components: + specifier: ^5.3.8 + version: 5.3.11(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react-is@19.2.4)(react@18.3.1) + swagger-ui-react: + specifier: ^5.29.4 + version: 5.32.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + ts-key-enum: + specifier: ^2.0.12 + version: 2.0.13 + unist-util-visit: + specifier: ^5.0.0 + version: 5.1.0 + url-parse: + specifier: ^1.5.9 + version: 1.5.10 + uuid: + specifier: ^8.3.2 + version: 8.3.2 + xstate: + specifier: ^4.38.3 + version: 4.38.3 + yup: + specifier: ^1.7.0 + version: 1.7.1 + devDependencies: + '@eslint-react/eslint-plugin': + specifier: ^1.53.0 + version: 1.53.1(eslint@9.32.0)(ts-api-utils@2.4.0(typescript@5.9.3))(typescript@5.9.3) + '@io-orkes/conductor-javascript': + specifier: ^2.1.4 + version: 2.4.1 + '@playwright/test': + specifier: ^1.56.0 + version: 1.58.2 + '@testing-library/dom': + specifier: ^10.4.0 + version: 10.4.1 + '@testing-library/jest-dom': + specifier: ^6.6.3 + version: 6.9.1 + '@testing-library/react': + specifier: ^16.3.0 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@testing-library/user-event': + specifier: ^14.6.1 + version: 14.6.1(@testing-library/dom@10.4.1) + '@types/autosuggest-highlight': + specifier: ^3.2.0 + version: 3.2.3 + '@types/dom-to-image': + specifier: ^2.6.7 + version: 2.6.7 + '@types/lodash': + specifier: ^4.14.178 + version: 4.17.24 + '@types/node': + specifier: ^24.10.1 + version: 24.12.0 + '@types/qs': + specifier: ^6.14.0 + version: 6.15.0 + '@types/react': + specifier: ^18.2.0 + version: 18.3.28 + '@types/react-beautiful-dnd': + specifier: ^13.1.3 + version: 13.1.8 + '@types/react-datepicker': + specifier: ^6.0.1 + version: 6.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/react-dom': + specifier: ^18.2.0 + version: 18.3.7(@types/react@18.3.28) + '@types/react-helmet': + specifier: ^6.1.5 + version: 6.1.11 + '@types/react-highlight': + specifier: ^0.12.8 + version: 0.12.8 + '@types/react-syntax-highlighter': + specifier: ^15.5.13 + version: 15.5.13 + '@types/swagger-ui-react': + specifier: ^5.18.0 + version: 5.18.0 + '@types/url-parse': + specifier: ^1.4.11 + version: 1.4.11 + '@types/uuid': + specifier: ^8.3.4 + version: 8.3.4 + '@types/yup': + specifier: ^0.32.0 + version: 0.32.0 + '@vitejs/plugin-react': + specifier: ^4.6.0 + version: 4.7.0(vite@7.3.1(@types/node@24.12.0)(sass@1.97.3)) + '@vitest/eslint-plugin': + specifier: ^1.3.4 + version: 1.6.10(eslint@9.32.0)(typescript@5.9.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jsdom@26.1.0)(sass@1.97.3)) + dotenv: + specifier: ^16.4.5 + version: 16.6.1 + eslint: + specifier: 9.32.0 + version: 9.32.0 + eslint-import-resolver-typescript: + specifier: ^3.6.1 + version: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0) + eslint-plugin-import: + specifier: ^2.28.1 + version: 2.32.0(@typescript-eslint/parser@8.57.0(eslint@9.32.0)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.32.0) + eslint-plugin-react-hooks: + specifier: ^5.2.0 + version: 5.2.0(eslint@9.32.0) + eslint-plugin-react-refresh: + specifier: ^0.4.20 + version: 0.4.26(eslint@9.32.0) + eslint-plugin-unused-imports: + specifier: ^3.0.0 + version: 3.2.0(eslint@9.32.0) + jsdom: + specifier: ^26.1.0 + version: 26.1.0 + monaco-editor: + specifier: ^0.55.1 + version: 0.55.1 + prettier: + specifier: ^3.6.2 + version: 3.8.1 + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + sass: + specifier: ^1.50.0 + version: 1.97.3 + typescript: + specifier: ^5.9.2 + version: 5.9.3 + typescript-eslint: + specifier: ^8.35.1 + version: 8.57.0(eslint@9.32.0)(typescript@5.9.3) + vite: + specifier: ^7.1.11 + version: 7.3.1(@types/node@24.12.0)(sass@1.97.3) + vite-plugin-dts: + specifier: ^4.5.0 + version: 4.5.4(@types/node@24.12.0)(rollup@4.59.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.12.0)(sass@1.97.3)) + vite-plugin-svgr: + specifier: ^4.3.0 + version: 4.5.0(rollup@4.59.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.12.0)(sass@1.97.3)) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.9.3)(vite@7.3.1(@types/node@24.12.0)(sass@1.97.3)) + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jsdom@26.1.0)(sass@1.97.3) + +packages: + + '@adobe/css-tools@4.4.4': + resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} + + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + + '@auth0/auth0-react@1.12.1': + resolution: {integrity: sha512-8+ecK/4rE0AGsxLW2IDcr1oPbT55tuE6cQEzEIOkQjB6QGQxxWMzQy0D4nMKw3JUAc7nYcFVOABNFNbc471n9Q==} + peerDependencies: + react: ^16.11.0 || ^17 || ^18 + react-dom: ^16.11.0 || ^17 || ^18 + + '@auth0/auth0-spa-js@1.22.6': + resolution: {integrity: sha512-iL3O0vWanfKFVgy1J2ZHDPlAUK6EVHWEHWS6mUXwHEuPiK39tjlQtyUKQIJI1F5YsZB75ijGgRWMTawSDXlwCA==} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.6': + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime-corejs3@7.29.0': + resolution: {integrity: sha512-TgUkdp71C9pIbBcHudc+gXZnihEDOjUAmXO1VO4HHGES7QLZcShR0stfKIxLSNIYx2fqhmJChOjm/wkF8wv4gA==} + engines: {node: '>=6.9.0'} + + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + + '@datasert/cronjs-matcher@1.4.0': + resolution: {integrity: sha512-5wAAKYfClZQDWjOeGReEnGLlBKds5K0CitnTv17sH32X4PSuck1dysX71zzCgrm0JCSpobDNg4b292ewhoy6ww==} + + '@datasert/cronjs-parser@1.4.0': + resolution: {integrity: sha512-zHGlrWanS4Zjgf0aMi/sp/HTSa2xWDEtXW9xshhlGf/jPx3zTIqfX14PZnoFF7XVOwzC49Zy0SFWG90rlRY36Q==} + + '@date-io/core@3.2.0': + resolution: {integrity: sha512-hqwXvY8/YBsT9RwQITG868ZNb1MVFFkF7W1Ecv4P472j/ZWa7EFcgSmxy8PUElNVZfvhdvfv+a8j6NWJqOX5mA==} + + '@date-io/dayjs@3.2.0': + resolution: {integrity: sha512-+3LV+3N+cpQbEtmrFo8odg07k02AFY7diHgbi2EKYYANOOCPkDYUjDr2ENiHuYNidTs3tZwzDKckZoVNN4NXxg==} + peerDependencies: + dayjs: ^1.8.17 + peerDependenciesMeta: + dayjs: + optional: true + + '@dnd-kit/accessibility@3.1.1': + resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} + peerDependencies: + react: '>=16.8.0' + + '@dnd-kit/core@6.3.1': + resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@dnd-kit/sortable@8.0.0': + resolution: {integrity: sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==} + peerDependencies: + '@dnd-kit/core': ^6.1.0 + react: '>=16.8.0' + + '@dnd-kit/utilities@3.2.2': + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=16.8.0' + + '@egjs/hammerjs@2.0.17': + resolution: {integrity: sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==} + engines: {node: '>=0.8.0'} + + '@emnapi/core@1.8.1': + resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} + + '@emnapi/runtime@1.8.1': + resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + + '@emnapi/wasi-threads@1.1.0': + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + + '@emotion/babel-plugin@11.13.5': + resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} + + '@emotion/cache@11.14.0': + resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==} + + '@emotion/hash@0.9.2': + resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} + + '@emotion/is-prop-valid@0.8.8': + resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==} + + '@emotion/is-prop-valid@1.4.0': + resolution: {integrity: sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==} + + '@emotion/memoize@0.7.4': + resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==} + + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + + '@emotion/react@11.14.0': + resolution: {integrity: sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==} + peerDependencies: + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@emotion/serialize@1.3.3': + resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==} + + '@emotion/sheet@1.4.0': + resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==} + + '@emotion/styled@11.14.1': + resolution: {integrity: sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==} + peerDependencies: + '@emotion/react': ^11.0.0-rc.0 + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@emotion/stylis@0.8.5': + resolution: {integrity: sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==} + + '@emotion/unitless@0.10.0': + resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} + + '@emotion/unitless@0.7.5': + resolution: {integrity: sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0': + resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==} + peerDependencies: + react: '>=16.8.0' + + '@emotion/utils@1.4.2': + resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==} + + '@emotion/weak-memoize@0.4.0': + resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint-react/ast@1.53.1': + resolution: {integrity: sha512-qvUC99ewtriJp9quVEOvZ6+RHcsMLfVQ0OhZ4/LupZUDhjW7GiX1dxJsFaxHdJ9rLNLhQyLSPmbAToeqUrSruQ==} + engines: {node: '>=18.18.0'} + + '@eslint-react/core@1.53.1': + resolution: {integrity: sha512-8prroos5/Uvvh8Tjl1HHCpq4HWD3hV9tYkm7uXgKA6kqj0jHlgRcQzuO6ZPP7feBcK3uOeug7xrq03BuG8QKCA==} + engines: {node: '>=18.18.0'} + + '@eslint-react/eff@1.53.1': + resolution: {integrity: sha512-uq20lPRAmsWRjIZm+mAV/2kZsU2nDqn5IJslxGWe3Vfdw23hoyhEw3S1KKlxbftwbTvsZjKvVP0iw3bZo/NUpg==} + engines: {node: '>=18.18.0'} + + '@eslint-react/eslint-plugin@1.53.1': + resolution: {integrity: sha512-JZ2ciXNCC9CtBBAqYtwWH+Jy/7ZzLw+whei8atP4Fxsbh+Scs30MfEwBzuiEbNw6uF9eZFfPidchpr5RaEhqxg==} + engines: {node: '>=18.18.0'} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: ^4.9.5 || ^5.3.3 + peerDependenciesMeta: + typescript: + optional: true + + '@eslint-react/kit@1.53.1': + resolution: {integrity: sha512-zOi2le9V4rMrJvQV4OeedGvMGvDT46OyFPOwXKs7m0tQu5vXVJ8qwIPaVQT1n/WIuvOg49OfmAVaHpGxK++xLQ==} + engines: {node: '>=18.18.0'} + + '@eslint-react/shared@1.53.1': + resolution: {integrity: sha512-gomJQmFqQgQVI3Ra4vTMG/s6a4bx3JqeNiTBjxBJt4C9iGaBj458GkP4LJHX7TM6xUzX+fMSKOPX7eV3C/+UCw==} + engines: {node: '>=18.18.0'} + + '@eslint-react/var@1.53.1': + resolution: {integrity: sha512-yzwopvPntcHU7mmDvWzRo1fb8QhjD8eDRRohD11rTV1u7nWO4QbJi0pOyugQakvte1/W11Y0Vr8Of0Ojk/A6zg==} + engines: {node: '>=18.18.0'} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.3.1': + resolution: {integrity: sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.15.2': + resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.32.0': + resolution: {integrity: sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.3.5': + resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/react@0.26.28': + resolution: {integrity: sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@growthbook/growthbook-react@1.6.5': + resolution: {integrity: sha512-afi/RUbwazVNKv2acn6wDQz4BJNRAEpwIuHfggQup2/aE5PLAxy3+95gjjRMgCcPR0Pf3sFmhYGvOmxLD0ZRbQ==} + engines: {node: '>=10'} + peerDependencies: + react: ^16.8.0-0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0 + + '@growthbook/growthbook@1.6.5': + resolution: {integrity: sha512-mUaMsgeUTpRIUOTn33EUXHRK6j7pxBjwqH4WpQyq+pukjd1AIzWlEa6w7i6bInJUcweGgP2beXZmaP6b6UPn7A==} + engines: {node: '>=10'} + + '@hookform/resolvers@5.2.2': + resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==} + peerDependencies: + react-hook-form: ^7.55.0 + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@io-orkes/conductor-javascript@2.4.1': + resolution: {integrity: sha512-2ANrH2jhwPdF5kGq9KPCBd45XYMkRp1lR/l7X0xRO5g4S+PmigPOeK7Lpy7INIt0RnvadjA3VH+tSC7goe0YVA==} + engines: {node: '>=18'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@jsonforms/core@3.7.0': + resolution: {integrity: sha512-CE9viWtwi9QWLqlWLeOul1/R1GRAyOA9y6OoUpsCc0FhyR+g5p29F3k0fUExHWxL0Sf4KHcXYkfhtqfRBPS8ww==} + + '@jsonforms/material-renderers@3.7.0': + resolution: {integrity: sha512-WO9D3zigJ/x/gCckEGxvfQgrdLuy6X6g76hHMlo3KCsusEvabLQHvYz3EJmOOBsuEu8JYXgZetTKjZ44WaBXww==} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@jsonforms/core': 3.7.0 + '@jsonforms/react': 3.7.0 + '@mui/icons-material': ^7.0.0 + '@mui/material': ^7.0.0 + '@mui/x-date-pickers': ^8.0.0 + react: ^16.12.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@jsonforms/react@3.7.0': + resolution: {integrity: sha512-HkY7qAx8vW97wPEgZ7GxCB3iiXG1c95GuObxtcDHGPBJWMwnxWBnVYJmv5h7nthrInKsQKHZL5OusnC/sj/1GQ==} + peerDependencies: + '@jsonforms/core': 3.7.0 + react: ^16.12.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@ljharb/resumer@0.0.1': + resolution: {integrity: sha512-skQiAOrCfO7vRTq53cxznMpks7wS1va95UCidALlOVWqvBAzwPVErwizDwoMqNVMEn1mDq0utxZd02eIrvF1lw==} + engines: {node: '>= 0.4'} + + '@ljharb/through@2.3.14': + resolution: {integrity: sha512-ajBvlKpWucBB17FuQYUShqpqy8GRgYEpJW0vWJbUu1CV9lWyrDCapy0lScU8T8Z6qn49sSwJB3+M+evYIdGg+A==} + engines: {node: '>= 0.4'} + + '@microsoft/api-extractor-model@7.33.4': + resolution: {integrity: sha512-u1LTaNTikZAQ9uK6KG1Ms7nvNedsnODnspq/gH2dcyETWvH4hVNGNDvRAEutH66kAmxA4/necElqGNs1FggC8w==} + + '@microsoft/api-extractor@7.57.7': + resolution: {integrity: sha512-kmnmVs32MFWbV5X6BInC1/TfCs7y1ugwxv1xHsAIj/DyUfoe7vtO0alRUgbQa57+yRGHBBjlNcEk33SCAt5/dA==} + hasBin: true + + '@microsoft/tsdoc-config@0.18.1': + resolution: {integrity: sha512-9brPoVdfN9k9g0dcWkFeA7IH9bbcttzDJlXvkf8b2OBzd5MueR1V2wkKBL0abn0otvmkHJC6aapBOTJDDeMCZg==} + + '@microsoft/tsdoc@0.16.0': + resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} + + '@monaco-editor/loader@1.7.0': + resolution: {integrity: sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==} + + '@monaco-editor/react@4.7.0': + resolution: {integrity: sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==} + peerDependencies: + monaco-editor: '>= 0.25.0 < 1' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@motionone/animation@10.18.0': + resolution: {integrity: sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw==} + + '@motionone/dom@10.18.0': + resolution: {integrity: sha512-bKLP7E0eyO4B2UaHBBN55tnppwRnaE3KFfh3Ps9HhnAkar3Cb69kUCJY9as8LrccVYKgHA+JY5dOQqJLOPhF5A==} + + '@motionone/easing@10.18.0': + resolution: {integrity: sha512-VcjByo7XpdLS4o9T8t99JtgxkdMcNWD3yHU/n6CLEz3bkmKDRZyYQ/wmSf6daum8ZXqfUAgFeCZSpJZIMxaCzg==} + + '@motionone/generators@10.18.0': + resolution: {integrity: sha512-+qfkC2DtkDj4tHPu+AFKVfR/C30O1vYdvsGYaR13W/1cczPrrcjdvYCj0VLFuRMN+lP1xvpNZHCRNM4fBzn1jg==} + + '@motionone/types@10.17.1': + resolution: {integrity: sha512-KaC4kgiODDz8hswCrS0btrVrzyU2CSQKO7Ps90ibBVSQmjkrt2teqta6/sOG59v7+dPnKMAg13jyqtMKV2yJ7A==} + + '@motionone/utils@10.18.0': + resolution: {integrity: sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw==} + + '@mui/base@5.0.0-beta.70': + resolution: {integrity: sha512-Tb/BIhJzb0pa5zv/wu7OdokY9ZKEDqcu1BDFnohyvGCoHuSXbEr90rPq1qeNW3XvTBIbNWHEF7gqge+xpUo6tQ==} + engines: {node: '>=14.0.0'} + deprecated: This package has been replaced by @base-ui/react + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@mui/core-downloads-tracker@7.3.9': + resolution: {integrity: sha512-MOkOCTfbMJwLshlBCKJ59V2F/uaLYfmKnN76kksj6jlGUVdI25A9Hzs08m+zjBRdLv+sK7Rqdsefe8X7h/6PCw==} + + '@mui/icons-material@7.3.9': + resolution: {integrity: sha512-BT+zPJXss8Hg/oEMRmHl17Q97bPACG4ufFSfGEdhiE96jOyR5Dz1ty7ZWt1fVGR0y1p+sSgEwQT/MNZQmoWDCw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@mui/material': ^7.3.9 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@mui/material@7.3.9': + resolution: {integrity: sha512-I8yO3t4T0y7bvDiR1qhIN6iBWZOTBfVOnmLlM7K6h3dx5YX2a7rnkuXzc2UkZaqhxY9NgTnEbdPlokR1RxCNRQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@mui/material-pigment-css': ^7.3.9 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@mui/material-pigment-css': + optional: true + '@types/react': + optional: true + + '@mui/private-theming@7.3.9': + resolution: {integrity: sha512-ErIyRQvsiQEq7Yvcvfw9UDHngaqjMy9P3JDPnRAaKG5qhpl2C4tX/W1S4zJvpu+feihmZJStjIyvnv6KDbIrlw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@mui/styled-engine@7.3.9': + resolution: {integrity: sha512-JqujWt5bX4okjUPGpVof/7pvgClqh7HvIbsIBIOOlCh2u3wG/Bwp4+E1bc1dXSwkrkp9WUAoNdI5HEC+5HKvMw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.4.1 + '@emotion/styled': ^11.3.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + + '@mui/system@7.3.9': + resolution: {integrity: sha512-aL1q9am8XpRrSabv9qWf5RHhJICJql34wnrc1nz0MuOglPRYF/liN+c8VqZdTvUn9qg+ZjRVbKf4sJVFfIDtmg==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + + '@mui/types@7.2.24': + resolution: {integrity: sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@mui/types@7.4.12': + resolution: {integrity: sha512-iKNAF2u9PzSIj40CjvKJWxFXJo122jXVdrmdh0hMYd+FR+NuJMkr/L88XwWLCRiJ5P1j+uyac25+Kp6YC4hu6w==} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@mui/utils@5.17.1': + resolution: {integrity: sha512-jEZ8FTqInt2WzxDV8bhImWBqeQRD99c/id/fq83H0ER9tFl+sfZlaAoCdznGvbSQQ9ividMxqSV2c7cC1vBcQg==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@mui/utils@6.4.9': + resolution: {integrity: sha512-Y12Q9hbK9g+ZY0T3Rxrx9m2m10gaphDuUMgWxyV5kNJevVxXYCLclYUCC9vXaIk1/NdNDTcW2Yfr2OGvNFNmHg==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@mui/utils@7.3.9': + resolution: {integrity: sha512-U6SdZaGbfb65fqTsH3V5oJdFj9uYwyLE2WVuNvmbggTSDBb8QHrFsqY8BN3taK9t3yJ8/BPHD/kNvLNyjwM7Yw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@mui/x-date-pickers@6.20.2': + resolution: {integrity: sha512-x1jLg8R+WhvkmUETRfX2wC+xJreMii78EXKLl6r3G+ggcAZlPyt0myID1Amf6hvJb9CtR7CgUo8BwR+1Vx9Ggw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.9.0 + '@emotion/styled': ^11.8.1 + '@mui/material': ^5.8.6 + '@mui/system': ^5.8.0 + date-fns: ^2.25.0 || ^3.2.0 + date-fns-jalali: ^2.13.0-0 + dayjs: ^1.10.7 + luxon: ^3.0.2 + moment: ^2.29.4 + moment-hijri: ^2.1.2 + moment-jalaali: ^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + date-fns: + optional: true + date-fns-jalali: + optional: true + dayjs: + optional: true + luxon: + optional: true + moment: + optional: true + moment-hijri: + optional: true + moment-jalaali: + optional: true + + '@napi-rs/wasm-runtime@0.2.12': + resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + + '@next/env@16.1.6': + resolution: {integrity: sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==} + + '@next/swc-darwin-arm64@16.1.6': + resolution: {integrity: sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@16.1.6': + resolution: {integrity: sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@16.1.6': + resolution: {integrity: sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-arm64-musl@16.1.6': + resolution: {integrity: sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@next/swc-linux-x64-gnu@16.1.6': + resolution: {integrity: sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-x64-musl@16.1.6': + resolution: {integrity: sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@next/swc-win32-arm64-msvc@16.1.6': + resolution: {integrity: sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@16.1.6': + resolution: {integrity: sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@nolyfill/is-core-module@1.0.39': + resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} + engines: {node: '>=12.4.0'} + + '@okta/okta-auth-js@6.9.0': + resolution: {integrity: sha512-IAh9mh2iGT4bsGeRMSSGBYoeEJ4f3ABTO+Jf9mYr0MbKgyU+X+7RwYAo/z8JHJ9AW0ynmjERTMOgDJ7/H/N+Dw==} + engines: {node: '>=11.0', yarn: ^1.7.0} + + '@okta/okta-react@6.10.0': + resolution: {integrity: sha512-0zgWeThYhH61GL0AfNHgvpBDzQNZcMDG/XOk4nkRdxckFKSg35vPgtKoF6IB6Khics8KzwiaAQCuXStcnLhbRQ==} + engines: {node: '>=10.3', yarn: ^1.7.0} + peerDependencies: + '@okta/okta-auth-js': ^5.3.1 || ^6.0.0 || ^7.0.0 + react: '>=16.8.0' + react-dom: '>=16.8.0' + react-router-dom: '>=5.1.0' + + '@okta/okta-signin-widget@6.9.0': + resolution: {integrity: sha512-UElSmcXRpRmYJqv2mz8IAnEjOK4Nt9fniw89bcmj3BK4q506oVgOheE7dxq5wnHLMYsJuuiDgc3gUrcFsrw+rw==} + engines: {node: '>=12.22', yarn: ^1.7.0} + + '@parcel/watcher-android-arm64@2.5.6': + resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.6': + resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.6': + resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.6': + resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.6': + resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm-musl@2.5.6': + resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-arm64-glibc@2.5.6': + resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm64-musl@2.5.6': + resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-x64-glibc@2.5.6': + resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-x64-musl@2.5.6': + resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@parcel/watcher-win32-arm64@2.5.6': + resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.6': + resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.6': + resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.6': + resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} + engines: {node: '>= 10.0.0'} + + '@peculiar/asn1-schema@2.6.0': + resolution: {integrity: sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==} + + '@peculiar/json-schema@1.1.12': + resolution: {integrity: sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==} + engines: {node: '>=8.0.0'} + + '@peculiar/webcrypto@1.5.0': + resolution: {integrity: sha512-BRs5XUAwiyCDQMsVA9IDvDa7UBR9gAvPHgugOeGng3YN6vJ9JYonyDc0lNczErgtCWtucjR5N7VtaonboD/ezg==} + engines: {node: '>=10.12.0'} + + '@phosphor-icons/react@2.1.10': + resolution: {integrity: sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==} + engines: {node: '>=10'} + peerDependencies: + react: '>= 16.8' + react-dom: '>= 16.8' + + '@playwright/test@1.58.2': + resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} + engines: {node: '>=18'} + hasBin: true + + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + cpu: [x64] + os: [win32] + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@rushstack/node-core-library@5.20.3': + resolution: {integrity: sha512-95JgEPq2k7tHxhF9/OJnnyHDXfC9cLhhta0An/6MlkDsX2A6dTzDrTUG18vx4vjc280V0fi0xDH9iQczpSuWsw==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + '@rushstack/problem-matcher@0.2.1': + resolution: {integrity: sha512-gulfhBs6n+I5b7DvjKRfhMGyUejtSgOHTclF/eONr8hcgF1APEDjhxIsfdUYYMzC3rvLwGluqLjbwCFZ8nxrog==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + '@rushstack/rig-package@0.7.2': + resolution: {integrity: sha512-9XbFWuqMYcHUso4mnETfhGVUSaADBRj6HUAAEYk50nMPn8WRICmBuCphycQGNB3duIR6EEZX3Xj3SYc2XiP+9A==} + + '@rushstack/terminal@0.22.3': + resolution: {integrity: sha512-gHC9pIMrUPzAbBiI4VZMU7Q+rsCzb8hJl36lFIulIzoceKotyKL3Rd76AZ2CryCTKEg+0bnTj406HE5YY5OQvw==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + '@rushstack/ts-command-line@5.3.3': + resolution: {integrity: sha512-c+ltdcvC7ym+10lhwR/vWiOhsrm/bP3By2VsFcs5qTKv+6tTmxgbVrtJ5NdNjANiV5TcmOZgUN+5KYQ4llsvEw==} + + '@scarf/scarf@1.4.0': + resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + + '@sindresorhus/to-milliseconds@1.2.0': + resolution: {integrity: sha512-nHpLEF6oRZJZ0ym8hmxz4jeSdnOqwWd5GC75GNQqNjfSG1IY55RE3AaGEC/QUDElLTuaPSBVa1rnV/C/rUkAUw==} + engines: {node: '>=8'} + + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + + '@svgr/babel-plugin-add-jsx-attribute@8.0.0': + resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-remove-jsx-attribute@8.0.0': + resolution: {integrity: sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0': + resolution: {integrity: sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0': + resolution: {integrity: sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-svg-dynamic-title@8.0.0': + resolution: {integrity: sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-svg-em-dimensions@8.0.0': + resolution: {integrity: sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-transform-react-native-svg@8.1.0': + resolution: {integrity: sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-transform-svg-component@8.0.0': + resolution: {integrity: sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==} + engines: {node: '>=12'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-preset@8.1.0': + resolution: {integrity: sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/core@8.1.0': + resolution: {integrity: sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==} + engines: {node: '>=14'} + + '@svgr/hast-util-to-babel-ast@8.0.0': + resolution: {integrity: sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==} + engines: {node: '>=14'} + + '@svgr/plugin-jsx@8.1.0': + resolution: {integrity: sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==} + engines: {node: '>=14'} + peerDependencies: + '@svgr/core': '*' + + '@swagger-api/apidom-ast@1.6.0': + resolution: {integrity: sha512-ez1KnBdAzoh5a6ijDXzu5nADkVZXlnL1RkLl8n2u2tjiNg9597xxmFdEHLVa31Vxr1yYj0WtYGLA5e2Kp0KNrQ==} + + '@swagger-api/apidom-core@1.6.0': + resolution: {integrity: sha512-gA1MVoXe19sjFLKGkWxp5VvSw3Tk0CSChfItJjFeFHpLSGrfm+LlXp37TmNSns53Ky0F7x7TB/5kAX5I/TO4xw==} + + '@swagger-api/apidom-error@1.6.0': + resolution: {integrity: sha512-xp/cQ1xQ/4Vd/hhQfONK7ea9oVc3JUXAYyfRzvDR0lxISly/SyD2jMcqXzHtrylBAnv7V2HSsbC1BWo7ZJDLSQ==} + + '@swagger-api/apidom-json-pointer@1.6.0': + resolution: {integrity: sha512-RO6P5Gt64AnthGXKeqIFjQCLVFbAJvLYAb67TkvRQ9US4lNixFtFsYJnhLCC4ymz4dTT1hacG0cmTRGcEHF9ig==} + + '@swagger-api/apidom-ns-api-design-systems@1.6.0': + resolution: {integrity: sha512-EYJfQ4JYuUo2J4QiiLnA/8LmM1k7AQcf1XVE+NrIpZ1160GIzqE+W5uOXkhAOImkP2Cb7EZZdE2cFE/tMYxNvw==} + + '@swagger-api/apidom-ns-arazzo-1@1.6.0': + resolution: {integrity: sha512-5rF8PyBiIHh6NfC5Y0WypW11X6hQIWr88EKNOQbBuT/nnzAsOznrUCfQ99FYGLucwdOHaMIBn/b/n4ejGBto/A==} + + '@swagger-api/apidom-ns-asyncapi-2@1.6.0': + resolution: {integrity: sha512-tOodfX+o7lonEAnSAxet7nCayW+EqtKPegT06WXt7Llq1LS9eYZ9YzXdFgIwCm8UzfEpZdVLqtxbdLX9vuUtSg==} + + '@swagger-api/apidom-ns-asyncapi-3@1.6.0': + resolution: {integrity: sha512-lRMvwTdtuPcwJEYLTX/UGtECpHi9UNYeT9rmWMw3LiKZrZzYc2L8q4ipPbpWwH8t7QfsF2u0iggCODU99lXCnw==} + + '@swagger-api/apidom-ns-json-schema-2019-09@1.6.0': + resolution: {integrity: sha512-dee1i8wcAFgDEOzTsyoCzQhFLZ2JKzkK5KkRuryabvwS0hG2mKlogToFc8cO2MkkiLSpERm7DREALwSTFVHa0w==} + + '@swagger-api/apidom-ns-json-schema-2020-12@1.6.0': + resolution: {integrity: sha512-ldTxSnnIXskwpN6yCJkasqs32pJXwoXyad95crKT0xjZZr4fTrcAXXIyzdjBubiY9tK6elSrQGQxinJcV7ivWw==} + + '@swagger-api/apidom-ns-json-schema-draft-4@1.6.0': + resolution: {integrity: sha512-t9HvHwrevEG7usosO6AdXmC8oYqje5nxHpUmODr72tUtCeAeGEGEb9lgqx7fBhjc3BYsRzOL1hX56m1gjEyCog==} + + '@swagger-api/apidom-ns-json-schema-draft-6@1.6.0': + resolution: {integrity: sha512-aoyvQWgAOcZGTe5OfJ3r24DvXHHbrkKtAnxTOEdZzV/uOm6/cbuT8m02+aMOqWPxei1naC3ZHW9iHrETtfgV3w==} + + '@swagger-api/apidom-ns-json-schema-draft-7@1.6.0': + resolution: {integrity: sha512-GjmC4+AHQh22fRZOmV+jSYMJTXh243XvdACfIQ//39kQu7gQsimF4PVSY2IgWSvS/I1ukWdPBYmDvOKryBPGrw==} + + '@swagger-api/apidom-ns-openapi-2@1.6.0': + resolution: {integrity: sha512-xbmYzagnB8rO7sYwNGVyxYbNBkjCWnMhlnMrxkPtfQ/2u2ANAmTnCB/S/cMswX5XofiRJbznKAjLDSKBS+mLpQ==} + + '@swagger-api/apidom-ns-openapi-3-0@1.6.0': + resolution: {integrity: sha512-AOvW7a2H27inepcTBAWaBMjJLrCh5IPWD4nTU+gysULC7IW6gphO8hj3iUuTmFBcGh9be89GBbvv2y/EGAfx9w==} + + '@swagger-api/apidom-ns-openapi-3-1@1.6.0': + resolution: {integrity: sha512-jCVypc8503zDSxAQlyV8j1vzwc75VBdWHtE2O0F+q5j9qNtGxw/ekbDkgrydYRaGBl92mf16dtPjtp5LwJD0Hw==} + + '@swagger-api/apidom-ns-openapi-3-2@1.6.0': + resolution: {integrity: sha512-QcFAUucaPaWiOKOEaaGqSfK3OtjeGJodWZLsuBQ0vrHaHkWyQ7jwsM1DJbc1Y8geOBeD2wIwdrdRjoulmqU1SA==} + + '@swagger-api/apidom-parser-adapter-api-design-systems-json@1.6.0': + resolution: {integrity: sha512-vz/9k0X/kh6mLm+Fi+LGNk/yyFq28wxI29ZVLW+b7ulcODikv+NaDnyN2n2kLKCvIchPATzAEvqMvVMuuQwWlg==} + + '@swagger-api/apidom-parser-adapter-api-design-systems-yaml@1.6.0': + resolution: {integrity: sha512-QAq4H6YzRtysSpvLtlJ8WZ22/1Mht+/iarrUOijxDZQPAGfYeUoIicnCqxkVZYSea85sQl+3kiCCB3nhSH+L0g==} + + '@swagger-api/apidom-parser-adapter-arazzo-json-1@1.6.0': + resolution: {integrity: sha512-syKPG3a9IGRvlGhXIEUzWhwbEuFbj+UwwtqaKu8zu771V+DRtH+wxyOkX54vKAIlApz/FgeUbmlWA1ZtYBlSIQ==} + + '@swagger-api/apidom-parser-adapter-arazzo-yaml-1@1.6.0': + resolution: {integrity: sha512-IVVLn+a8Q1iQcQsm4tXiAPghHJuJSB1rhIlDyHe3tSQgt9HOSiVpbnJDpwE/JBxxDxSAkeT6Ovo+fi2T5AmHYg==} + + '@swagger-api/apidom-parser-adapter-asyncapi-json-2@1.6.0': + resolution: {integrity: sha512-aSUi22ELTDvdCLA3nIUOehuNBcHSeCqU7S7YNiHP/mwE4Q07pwQrYXijH2PROfCdjlZNNN34m6Ptakd92jliJQ==} + + '@swagger-api/apidom-parser-adapter-asyncapi-json-3@1.6.0': + resolution: {integrity: sha512-Ic53vcFF9zniDyCXOGSwwuAdEBUn5lFEAa0m2i30R36cQFHBCCuvbzbMQjWdr+oML0Aw4XoqOwZCQgkJJICpPA==} + + '@swagger-api/apidom-parser-adapter-asyncapi-yaml-2@1.6.0': + resolution: {integrity: sha512-d/w7X+T4vT+KPqb+8xUm6n4pbHsGB28jdxE9rNVbxhu6D3owny2uxfglwaFh4fJG6FQMavCwl/QzfB4newdoKQ==} + + '@swagger-api/apidom-parser-adapter-asyncapi-yaml-3@1.6.0': + resolution: {integrity: sha512-Wmf0LY59TZxQhqrJU2pcnUikcChVB4IqGPgjtOFLUoqPpz8FSwYbJ/SPnSMSl+QuncxROheSFsgZ6Tupv0sPHw==} + + '@swagger-api/apidom-parser-adapter-json@1.6.0': + resolution: {integrity: sha512-WdAS+dBAB2t18HuUgSZy5b8JM7uXfn1RlPymJNRMUsrKYCTtPrQ/0q3YfnBjPhtjSSNCp+p1wajxHAFS7cj2VA==} + + '@swagger-api/apidom-parser-adapter-openapi-json-2@1.6.0': + resolution: {integrity: sha512-Q36W1FzdVaY7Oh98533dzCUghwb8k3ZMdlnV37V1H13FlUkj3tVZiWaeaCLwIakzQ7XXYaQTOP+VrRhDRjzhUA==} + + '@swagger-api/apidom-parser-adapter-openapi-json-3-0@1.6.0': + resolution: {integrity: sha512-UY+obOLTPHJvnXscdMY9XwZyuqcnBe6cu9TURjJgkO/QpOpPDqqZoRyurKZgRrX0Pv9B1zR3EIzhl01u/jeUaw==} + + '@swagger-api/apidom-parser-adapter-openapi-json-3-1@1.6.0': + resolution: {integrity: sha512-4ch04/96lYMXQu6odqa6H0aJmV8UefnBJKX1CPuL4qcPSPMFCurcXHGpPHrwMu1p/4Q9H+yRVlYeNQV10xvM0w==} + + '@swagger-api/apidom-parser-adapter-openapi-json-3-2@1.6.0': + resolution: {integrity: sha512-fWR2gjMQg00QIimcXQMSVeLnCH/2iuDD/Dx8TzVHmKV/IKlu+TnmIVosdlDfRmOB+4duwU6/yfoA79IEhFeZdw==} + + '@swagger-api/apidom-parser-adapter-openapi-yaml-2@1.6.0': + resolution: {integrity: sha512-dkEh1Rw9uvuIAOTfKjWRX2rLWP+xJ/Eqdkqeo0I0BWFKXX49YcDpHJV4XHpmd5FbsjJ9vBYr0hAmkbl32TtR4g==} + + '@swagger-api/apidom-parser-adapter-openapi-yaml-3-0@1.6.0': + resolution: {integrity: sha512-6azq5YonWdzHcO9llK9zn1a+rGxlTz2Uf8p8NWDQnl2AZ56neDLYEL3mNDlrMXAy8dSJIHw+u9VF1OOzdslIHQ==} + + '@swagger-api/apidom-parser-adapter-openapi-yaml-3-1@1.6.0': + resolution: {integrity: sha512-g2tGCXyIAC0IA6JjA0HVxHWyCovyfAxDQ+pMAJ6qm4PfrZHB+oXKWKZHNNmQaFiKdc/SVdMQq6Up0mXOQs7IOQ==} + + '@swagger-api/apidom-parser-adapter-openapi-yaml-3-2@1.6.0': + resolution: {integrity: sha512-NGkdG9X5Svi89ZBluNseyUBNdgB9MkbTTNmerVKKOmCCHaVbzIb6UFPXf1MifSFyT+wTeGZk6WZLgRIDsTAZ5Q==} + + '@swagger-api/apidom-parser-adapter-yaml-1-2@1.6.0': + resolution: {integrity: sha512-UwSE5pPUJ+ag7ZCbesgx/SJ8zUD3Sx+2U4AD3/1G1EJ+0gb7FMYgihuOT8ujmBfZVGGm3HMIEIa1w3zha08v2g==} + + '@swagger-api/apidom-reference@1.6.0': + resolution: {integrity: sha512-gYTDfWQM1heqrCCrCsZH+EWDyAkIGqEJnSJcVWKngwOkXJKeUwat8p1TOW4q3rkaTT+fBaYbrjTr9SkFtVbdMg==} + + '@swaggerexpert/cookie@2.0.2': + resolution: {integrity: sha512-DPI8YJ0Vznk4CT+ekn3rcFNq1uQwvUHZhH6WvTSPD0YKBIlMS9ur2RYKghXuxxOiqOam/i4lHJH4xTIiTgs3Mg==} + engines: {node: '>=12.20.0'} + + '@swaggerexpert/json-pointer@2.10.2': + resolution: {integrity: sha512-qMx1nOrzoB+PF+pzb26Q4Tc2sOlrx9Ba2UBNX9hB31Omrq+QoZ2Gly0KLrQWw4Of1AQ4J9lnD+XOdwOdcdXqqw==} + engines: {node: '>=12.20.0'} + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@testing-library/user-event@14.6.1': + resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + + '@tree-sitter-grammars/tree-sitter-yaml@0.7.1': + resolution: {integrity: sha512-AynBwkIoQCTgjDR33bDUp9Mqq+YTco0is3n5hRApMqG9of/6A4eQsfC1/uSEeHSUyMQSYawcAWamsexnVpIP4Q==} + peerDependencies: + tree-sitter: ^0.22.4 + peerDependenciesMeta: + tree-sitter: + optional: true + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/argparse@1.0.38': + resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/autosuggest-highlight@3.2.3': + resolution: {integrity: sha512-8Mb21KWtpn6PvRQXjsKhrXIcxbSloGqNH50RntwGeJsGPW4xvNhfml+3kKulaKpO/7pgZfOmzsJz7VbepArlGQ==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/backbone@1.4.23': + resolution: {integrity: sha512-B/hN/DAJdWFOusEkEoa5xgfVuxJJPOR/6JQ2uwURPDyKL24PuC76IR6EcSRJ6lvGWtxHRQYMJQhJYm6KpMDtGQ==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/dom-to-image@2.6.7': + resolution: {integrity: sha512-me5VbCv+fcXozblWwG13krNBvuEOm6kA5xoa4RrjDJCNFOZSWR3/QLtOXimBHk1Fisq69Gx3JtOoXtg1N1tijg==} + + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/hammerjs@2.0.46': + resolution: {integrity: sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==} + + '@types/hast@2.3.10': + resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/jquery@3.5.34': + resolution: {integrity: sha512-3m3939S3erqmTLJANS/uy0B6V7BorKx7RorcGZVjZ62dF5PAGbKEDZK1CuLtKombJkFA2T1jl8LAIIs7IV6gBQ==} + + '@types/jqueryui@1.12.24': + resolution: {integrity: sha512-E2sGULwzMhg4kAeOV+gYcXjg988RuPkviWCt09jLe6GGK9sHM7dTqS8H7JMuUWoZQBucIBzBAgM5o/ezKUFkeg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/lodash@4.17.24': + resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@24.12.0': + resolution: {integrity: sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==} + + '@types/parse-json@4.0.2': + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + + '@types/prismjs@1.26.6': + resolution: {integrity: sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/q@1.5.8': + resolution: {integrity: sha512-hroOstUScF6zhIi+5+x0dzqrHA1EJi+Irri6b1fxolMTqqHIV/Cg77EtnQcZqZCu8hR3mX2BzIxN4/GzI68Kfw==} + + '@types/qs@6.15.0': + resolution: {integrity: sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==} + + '@types/ramda@0.30.2': + resolution: {integrity: sha512-PyzHvjCalm2BRYjAU6nIB3TprYwMNOUY/7P/N8bSzp9W/yM2YrtGtAnnVtaCNSeOZ8DzKyFDvaqQs7LnWwwmBA==} + + '@types/react-beautiful-dnd@13.1.8': + resolution: {integrity: sha512-E3TyFsro9pQuK4r8S/OL6G99eq7p8v29sX0PM7oT8Z+PJfZvSQTx4zTQbUJ+QZXioAF0e7TGBEcA1XhYhCweyQ==} + + '@types/react-datepicker@6.2.0': + resolution: {integrity: sha512-+JtO4Fm97WLkJTH8j8/v3Ldh7JCNRwjMYjRaKh4KHH0M3jJoXtwiD3JBCsdlg3tsFIw9eQSqyAPeVDN2H2oM9Q==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react-helmet@6.1.11': + resolution: {integrity: sha512-0QcdGLddTERotCXo3VFlUSWO3ztraw8nZ6e3zJSgG7apwV5xt+pJUS8ewPBqT4NYB1optGLprNQzFleIY84u/g==} + + '@types/react-highlight@0.12.8': + resolution: {integrity: sha512-V7O7zwXUw8WSPd//YUO8sz489J/EeobJljASGhP0rClrvq+1Y1qWEpToGu+Pp7YuChxhAXSgkLkrOYpZX5A62g==} + + '@types/react-syntax-highlighter@15.5.13': + resolution: {integrity: sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==} + + '@types/react-transition-group@4.4.12': + resolution: {integrity: sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==} + peerDependencies: + '@types/react': '*' + + '@types/react@18.3.28': + resolution: {integrity: sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==} + + '@types/selectize@0.12.39': + resolution: {integrity: sha512-ABnSEXM1MyO9ZZXl2yXLqzHcENuGh6kyXisnq87OQCubbJrMaargMYV/NPVmJA3lJGnDM6hzc1ce7yQM/RwI5g==} + + '@types/sizzle@2.3.10': + resolution: {integrity: sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==} + + '@types/swagger-ui-react@5.18.0': + resolution: {integrity: sha512-c2M9adVG7t28t1pq19K9Jt20VLQf0P/fwJwnfcmsVVsdkwCWhRmbKDu+tIs0/NGwJ/7GY8lBx+iKZxuDI5gDbw==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/underscore@1.13.0': + resolution: {integrity: sha512-L6LBgy1f0EFQZ+7uSA57+n2g/s4Qs5r06Vwrwn0/nuK1de+adz00NWaztRQ30aEqw5qOaWbPI8u2cGQ52lj6VA==} + + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/url-parse@1.4.11': + resolution: {integrity: sha512-FKvKIqRaykZtd4n47LbK/W/5fhQQ1X7cxxzG9A48h0BGN+S04NH7ervcCjM8tyR0lyGru83FAHSmw2ObgKoESg==} + + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + + '@types/uuid@8.3.4': + resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==} + + '@types/yup@0.32.0': + resolution: {integrity: sha512-Gr2lllWTDxGVYHgWfL8szjdedERpNgm44L9BDL2cmcHG7Bfd6taEpiW3ayMFLaYvlJr/6bFXDJdh6L406AGlFg==} + deprecated: This is a stub types definition. yup provides its own type definitions, so you do not need this installed. + + '@typescript-eslint/eslint-plugin@8.57.0': + resolution: {integrity: sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.57.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.57.0': + resolution: {integrity: sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.57.0': + resolution: {integrity: sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.57.0': + resolution: {integrity: sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.57.0': + resolution: {integrity: sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.57.0': + resolution: {integrity: sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.57.0': + resolution: {integrity: sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.57.0': + resolution: {integrity: sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.57.0': + resolution: {integrity: sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.57.0': + resolution: {integrity: sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.11.1': + resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.11.1': + resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} + cpu: [x64] + os: [win32] + + '@use-gesture/core@10.3.1': + resolution: {integrity: sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==} + + '@use-gesture/react@10.3.1': + resolution: {integrity: sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==} + peerDependencies: + react: '>= 16.8.0' + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + '@vitest/eslint-plugin@1.6.10': + resolution: {integrity: sha512-/cOf+mTu4HBJIYHTETo8/OFCSZv3T2p+KfGnouzKfjK063cWLZp0TzvK7EU5B3eFG7ypUNtw6l+jK+SA+p1g8g==} + engines: {node: '>=18'} + peerDependencies: + eslint: '>=8.57.0' + typescript: '>=5.0.0' + vitest: '*' + peerDependenciesMeta: + typescript: + optional: true + vitest: + optional: true + + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + + '@volar/language-core@2.4.28': + resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} + + '@volar/source-map@2.4.28': + resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==} + + '@volar/typescript@2.4.28': + resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} + + '@vue/compiler-core@3.5.30': + resolution: {integrity: sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==} + + '@vue/compiler-dom@3.5.30': + resolution: {integrity: sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==} + + '@vue/compiler-vue2@2.7.16': + resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} + + '@vue/language-core@2.2.0': + resolution: {integrity: sha512-O1ZZFaaBGkKbsRfnVH1ifOK1/1BUkyK+3SQsfnh6PmMmD4qJcTU8godCeA96jjDRTL6zgnK7YzCHfaUlH2r0Mw==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/shared@3.5.30': + resolution: {integrity: sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==} + + '@xstate/inspect@0.8.0': + resolution: {integrity: sha512-wSkFeOnp+7dhn+zTThO0M4D2FEqZN9lGIWowJu5JLa2ojjtlzRwK8SkjcHZ4rLX8VnMev7kGjgQLrGs8kxy+hw==} + peerDependencies: + '@types/ws': ^8.0.0 + ws: ^8.0.0 + xstate: ^4.37.0 + peerDependenciesMeta: + '@types/ws': + optional: true + + '@xstate/react@3.2.2': + resolution: {integrity: sha512-feghXWLedyq8JeL13yda3XnHPZKwYDN5HPBLykpLeuNpr9178tQd2/3d0NrH6gSd0sG5mLuLeuD+ck830fgzLQ==} + peerDependencies: + '@xstate/fsm': ^2.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + xstate: ^4.37.2 + peerDependenciesMeta: + '@xstate/fsm': + optional: true + xstate: + optional: true + + Base64@1.1.0: + resolution: {integrity: sha512-qeacf8dvGpf+XAT27ESHMh7z84uRzj/ua2pQdJg483m3bEXv/kVFtDnMgvf70BQGqzbZhR9t6BmASzKvqfJf3Q==} + + abortcontroller-polyfill@1.7.8: + resolution: {integrity: sha512-9f1iZ2uWh92VcrU9Y8x+LdM4DLj75VE0MJB8zuF1iUnroEptStw+DQ8EQPMUdfe5k+PkB1uUfDQfWbhstH8LrQ==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv-draft-04@1.0.0: + resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} + peerDependencies: + ajv: ^8.5.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-errors@3.0.0: + resolution: {integrity: sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==} + peerDependencies: + ajv: ^8.0.1 + + ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + + alien-signals@0.4.14: + resolution: {integrity: sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + apg-lite@1.0.5: + resolution: {integrity: sha512-SlI+nLMQDzCZfS39ihzjGp3JNBQfJXyMi6cg9tkLOCPVErgFsUIAEdO9IezR7kbP5Xd0ozcPNQBkf9TO5cHgWw==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + asn1js@3.0.7: + resolution: {integrity: sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ==} + engines: {node: '>=12.0.0'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + atob@2.1.2: + resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} + engines: {node: '>= 4.5.0'} + hasBin: true + + autolinker@3.16.2: + resolution: {integrity: sha512-JiYl7j2Z19F9NdTmirENSUUIIL/9MytEWtmzhfmsKPCp9E+G35Y0UNCMoM9tFigxT59qSc8Ml2dlZXOCVTYwuA==} + + autosuggest-highlight@3.3.4: + resolution: {integrity: sha512-j6RETBD2xYnrVcoV1S5R4t3WxOlWZKyDQjkwnggDPSjF5L4jV98ZltBpvPvbkM1HtoSe5o+bNrTHyjPbieGeYA==} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axios@1.13.6: + resolution: {integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==} + + babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + + babel-plugin-styled-components@2.1.4: + resolution: {integrity: sha512-Xgp9g+A/cG47sUyRwwYxGM4bR/jDRg5N6it/8+HxCnbT5XNKSKDT9xm4oag/osgqjC2It/vH0yXsomOG6k558g==} + peerDependencies: + styled-components: '>= 2' + + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.10.0: + resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==} + engines: {node: '>=6.0.0'} + hasBin: true + + batch-processor@1.0.0: + resolution: {integrity: sha512-xoLQD8gmmR32MeuBHgH0Tzd5PuSZx71ZsbhVxOCRbgktZEPe4SQy7s9Z50uPp0F/f7iw2XmkHN2xkgbMfckMDA==} + + big-integer@1.6.52: + resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} + engines: {node: '>=0.6'} + + birecord@0.1.1: + resolution: {integrity: sha512-VUpsf/qykW0heRlC8LooCq28Kxn3mAqKohhDG/49rrsQ1dT1CXyj/pgXS+5BSRzFTR/3DyIBOqQOrGyZOh71Aw==} + + body-scroll-lock-upgrade@1.1.0: + resolution: {integrity: sha512-nnfVAS+tB7CS9RaksuHVTpgHWHF7fE/ptIBJnwZrMqImIvWJF1OGcLnMpBhC6qhkx9oelvyxmWXwmIJXCV98Sw==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + brace-expansion@5.0.4: + resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + engines: {node: 18 || 20 || >=22} + + broadcast-channel@3.7.0: + resolution: {integrity: sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==} + + broadcast-channel@4.17.0: + resolution: {integrity: sha512-r2GSQMNgZv7eAsbdsu9xofSjc3J2diCQTPkSuyVhLBfx8fylLCVhi5KheuhuAQBJNd4pxqUyz9U6rvdnt7GZng==} + + browser-tabs-lock@1.3.0: + resolution: {integrity: sha512-g6nHaobTiT0eMZ7jh16YpD2kcjAp+PInbiVq3M1x6KKaEIVhT4v9oURNIpZLOZ3LQbQ3XYfNhMAb/9hzNLIWrw==} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + btoa@1.2.1: + resolution: {integrity: sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==} + engines: {node: '>= 0.4.0'} + hasBin: true + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + calculate-size@1.1.1: + resolution: {integrity: sha512-jJZ7pvbQVM/Ss3VO789qpsypN3xmnepg242cejOAslsmlZLYw2dnj7knnNowabQ0Kzabzx56KFTy2Pot/y6FmA==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + camelize@1.0.1: + resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} + + caniuse-lite@1.0.30001777: + resolution: {integrity: sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@1.1.4: + resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@1.2.4: + resolution: {integrity: sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@1.1.4: + resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + clipboard@1.7.1: + resolution: {integrity: sha512-smkaRaIQsrnKN1F3wd1/vY9Q+DeR4L8ZCXKeHCFC2j8RZuSBbuImcLdnIO4GTxmzJxQuDGNKkyfpGoPW7Ua5bQ==} + + clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + comma-separated-tokens@1.0.8: + resolution: {integrity: sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + compare-versions@4.1.4: + resolution: {integrity: sha512-FemMreK9xNyL8gQevsdRMrvO4lFCkQP7qbuktn1q8ndcNk1+0mz7lgE7b/sNvbhVgY4w6tMN1FDp6aADjqw2rw==} + + compare-versions@6.1.1: + resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} + + component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + + container-query-toolkit@0.1.3: + resolution: {integrity: sha512-B1EvYaLzFKz81vgWDm+zL0X7fzFUjlN6lF/RivDeNT4xW9mFsTh1oiC9rtvFFiwG52e3JUmYLXwPpqNBf2AXHA==} + + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + copy-to-clipboard@3.3.3: + resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} + + core-js-pure@3.48.0: + resolution: {integrity: sha512-1slJgk89tWC51HQ1AEqG+s2VuwpTRr8ocu4n20QUcH1v9lAN0RXen0Q0AABa/DK1I7RrNWLucplOHMx8hfTGTw==} + + core-js@3.48.0: + resolution: {integrity: sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==} + + cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + + cosmiconfig@8.3.6: + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + cron-validate@1.5.3: + resolution: {integrity: sha512-jcu8g/3wZL8OBr4MkEcbeIdLpM8pp5Y6UoOlRktcJG3WjgpifijR0s26Yac7ywR0gC2ABtevOsz5mlD3l3gzwA==} + + cronstrue@2.61.0: + resolution: {integrity: sha512-ootN5bvXbIQI9rW94+QsXN5eROtXWwew6NkdGxIRpS/UFWRggL0G5Al7a9GTBFEsuvVhJ2K3CntIIVt7L2ILhA==} + deprecated: Non-backwards compatible Breaking changes + hasBin: true + + cross-fetch@3.2.0: + resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-color-keywords@1.0.0: + resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==} + engines: {node: '>=4'} + + css-to-react-native@3.2.0: + resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + date-fns-tz@2.0.1: + resolution: {integrity: sha512-fJCG3Pwx8HUoLhkepdsP7Z5RsucUi+ZBOxyM5d0ZZ6c4SdYustq0VMmOu6Wf7bli+yS/Jwp91TOCqn9jMcVrUA==} + peerDependencies: + date-fns: 2.x + + date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + + date-fns@3.6.0: + resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} + + dayjs@1.10.7: + resolution: {integrity: sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig==} + + de-indent@1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + + deep-copy@1.4.2: + resolution: {integrity: sha512-VxZwQ/1+WGQPl5nE67uLhh7OqdrmqI1OazrraO9Bbw/M8Bt6Mol/RxzDA6N6ZgRXpsG/W9PgUj8E1LHHBEq2GQ==} + engines: {node: '>=4.0.0'} + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-equal@1.1.2: + resolution: {integrity: sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==} + engines: {node: '>= 0.4'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + defaulty@2.1.0: + resolution: {integrity: sha512-dNWjHNxL32khAaX/kS7/a3rXsgvqqp7cptqt477wAVnJLgaOKjcQt+53jKgPofn6hL2xyG51MegPlB5TKImXjA==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + defined@1.0.1: + resolution: {integrity: sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + delegate@3.2.0: + resolution: {integrity: sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + detect-node@2.1.0: + resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + diff@8.0.3: + resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} + engines: {node: '>=0.3.1'} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + + dom-mutator@0.6.0: + resolution: {integrity: sha512-iCt9o0aYfXMUkz/43ZOAUFQYotjGB+GNbYJiJdz4TgXkyToXbbRy5S6FbTp72lRBtfpUMwEc1KmpFEU4CZeoNg==} + engines: {node: '>=10'} + + dom-to-image@2.6.0: + resolution: {integrity: sha512-Dt0QdaHmLpjURjU7Tnu3AgYSF2LuOmksSGsUcE6ItvJoCWTBEmiMXcqBdNSAm9+QbbwD7JMoVsuuKX6ZVQv1qA==} + + dompurify@3.2.6: + resolution: {integrity: sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==} + + dompurify@3.2.7: + resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==} + + dot-case@3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dotignore@0.1.2: + resolution: {integrity: sha512-UGGGWfSauusaVJC+8fgV+NVvBXkCTmVv7sk6nojDZZvuOUNGUy0Zk4UpHQD6EDjS0jpBwcACvH4eofvyzBcRDw==} + hasBin: true + + drange@1.1.1: + resolution: {integrity: sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==} + engines: {node: '>=4'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + electron-to-chromium@1.5.307: + resolution: {integrity: sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==} + + element-resize-detector@1.1.13: + resolution: {integrity: sha512-QzMTvOM+hSXzPGxO4XeHq8OJAJZ/0kZQRbIBVGlR4GRVWHdfv/I/udYzIcQCZtzN1LdwkrGsNPWTIDbC8Tj7PA==} + + elkjs@0.8.2: + resolution: {integrity: sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==} + + ellipsize@0.2.0: + resolution: {integrity: sha512-InJhblLPZbBjw3N49knOWonfprgKPLKGySmG6bGHi7WsD5OkXIIlLkU4AguROmaMZ0v1BRdo267wEc0Pexw8ww==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-abstract@1.24.1: + resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} + engines: {node: '>= 0.4'} + + es-cookie@1.3.2: + resolution: {integrity: sha512-UTlYYhXGLOy05P/vKVT2Ui7WtC7NiRzGtJyAKKn32g5Gvcjn7KAClLPWlipCtxIus934dFg9o9jXiBL0nP+t9Q==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-import-resolver-typescript@3.10.1: + resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + + eslint-module-utils@2.12.1: + resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-react-debug@1.53.1: + resolution: {integrity: sha512-WNOiQ6jhodJE88VjBU/IVDM+2Zr9gKHlBFDUSA3fQ0dMB5RiBVj5wMtxbxRuipK/GqNJbteqHcZoYEod7nfddg==} + engines: {node: '>=18.18.0'} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: ^4.9.5 || ^5.3.3 + peerDependenciesMeta: + typescript: + optional: true + + eslint-plugin-react-dom@1.53.1: + resolution: {integrity: sha512-UYrWJ2cS4HpJ1A5XBuf1HfMpPoLdfGil+27g/ldXfGemb4IXqlxHt4ANLyC8l2CWcE3SXGJW7mTslL34MG0qTQ==} + engines: {node: '>=18.18.0'} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: ^4.9.5 || ^5.3.3 + peerDependenciesMeta: + typescript: + optional: true + + eslint-plugin-react-hooks-extra@1.53.1: + resolution: {integrity: sha512-fshTnMWNn9NjFLIuy7HzkRgGK29vKv4ZBO9UMr+kltVAfKLMeXXP6021qVKk66i/XhQjbktiS+vQsu1Rd3ZKvg==} + engines: {node: '>=18.18.0'} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: ^4.9.5 || ^5.3.3 + peerDependenciesMeta: + typescript: + optional: true + + eslint-plugin-react-hooks@5.2.0: + resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react-naming-convention@1.53.1: + resolution: {integrity: sha512-rvZ/B/CSVF8d34HQ4qIt90LRuxotVx+KUf3i1OMXAyhsagEFMRe4gAlPJiRufZ+h9lnuu279bEdd+NINsXOteA==} + engines: {node: '>=18.18.0'} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: ^4.9.5 || ^5.3.3 + peerDependenciesMeta: + typescript: + optional: true + + eslint-plugin-react-refresh@0.4.26: + resolution: {integrity: sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==} + peerDependencies: + eslint: '>=8.40' + + eslint-plugin-react-web-api@1.53.1: + resolution: {integrity: sha512-INVZ3Cbl9/b+sizyb43ChzEPXXYuDsBGU9BIg7OVTNPyDPloCXdI+dQFAcSlDocZhPrLxhPV3eT6+gXbygzYXg==} + engines: {node: '>=18.18.0'} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: ^4.9.5 || ^5.3.3 + peerDependenciesMeta: + typescript: + optional: true + + eslint-plugin-react-x@1.53.1: + resolution: {integrity: sha512-MwMNnVwiPem0U6SlejDF/ddA4h/lmP6imL1RDZ2m3pUBrcdcOwOx0gyiRVTA3ENnhRlWfHljHf5y7m8qDSxMEg==} + engines: {node: '>=18.18.0'} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + ts-api-utils: ^2.1.0 + typescript: ^4.9.5 || ^5.3.3 + peerDependenciesMeta: + ts-api-utils: + optional: true + typescript: + optional: true + + eslint-plugin-unused-imports@3.2.0: + resolution: {integrity: sha512-6uXyn6xdINEpxE1MtDjxQsyXB37lfyO2yKGVVgtD7WEWQGORSOZjgrD6hBhvGv4/SO+TOlS+UnC6JppRqbuwGQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/eslint-plugin': 6 - 7 + eslint: '8' + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + + eslint-rule-composer@0.3.0: + resolution: {integrity: sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==} + engines: {node: '>=4.0.0'} + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.32.0: + resolution: {integrity: sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-deep-equal@1.1.0: + resolution: {integrity: sha512-fueX787WZKCV0Is4/T2cyAdM4+x1S3MXXOAhavE1ys/W42SHAPacLTQhucja22QBYrfGw50M2sRiXPtTGv9Ymw==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-equals@5.4.0: + resolution: {integrity: sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==} + engines: {node: '>=6.0.0'} + + fast-json-patch@3.1.1: + resolution: {integrity: sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + fast-text-encoding@1.0.6: + resolution: {integrity: sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fault@1.0.4: + resolution: {integrity: sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.1: + resolution: {integrity: sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + format@0.2.2: + resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} + engines: {node: '>=0.4.x'} + + framer-motion@10.18.0: + resolution: {integrity: sha512-oGlDh1Q1XqYPksuTD/usb0I70hq95OUzmL9+6Zd+Hs4XV0oaISBa/UUMSjYiq6m8EUF32132mOJ8xVZS+I0S6w==} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + + framer-motion@7.10.3: + resolution: {integrity: sha512-k2ccYeZNSpPg//HTaqrU+4pRq9f9ZpaaN7rr0+Rx5zA4wZLbk547wtDzge2db1sB+1mnJ6r59P4xb+aEIi/W+w==} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + + fs-extra@11.3.4: + resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==} + engines: {node: '>=14.14'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + fuse.js@6.6.2: + resolution: {integrity: sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==} + engines: {node: '>=10'} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.13.6: + resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + + good-listener@1.2.2: + resolution: {integrity: sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + hammerjs@2.0.8: + resolution: {integrity: sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==} + engines: {node: '>=0.8.0'} + + handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + has@1.0.4: + resolution: {integrity: sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==} + engines: {node: '>= 0.4.0'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hast-util-parse-selector@2.2.5: + resolution: {integrity: sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==} + + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hastscript@6.0.0: + resolution: {integrity: sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==} + + hastscript@9.0.1: + resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + hey-listen@1.0.8: + resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==} + + highlight.js@10.7.3: + resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + + highlight.js@11.11.1: + resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} + engines: {node: '>=12.0.0'} + + highlightjs-vue@1.0.0: + resolution: {integrity: sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==} + + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + immutable@3.8.3: + resolution: {integrity: sha512-AUY/VyX0E5XlibOmWt10uabJzam1zlYjwiEgQSDc5+UIkFNaF9WM0JxXKaNMGf+F/ffUF+7kRKXM9A7C0xXqMg==} + engines: {node: '>=0.10.0'} + + immutable@5.1.5: + resolution: {integrity: sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-lazy@4.0.0: + resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} + engines: {node: '>=8'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + + is-alphabetical@1.0.4: + resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} + + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@1.0.4: + resolution: {integrity: sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-arrayish@0.3.4: + resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-decimal@1.0.4: + resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==} + + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-hexadecimal@1.0.4: + resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==} + + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + + is-immutable-type@5.0.1: + resolution: {integrity: sha512-LkHEOGVZZXxGl8vDs+10k3DvP++SEoYEAJLRk6buTFi6kD7QekThV7xHS0j6gpnUCQ0zpud/gMDGiV4dQneLTg==} + peerDependencies: + eslint: '*' + typescript: '>=4.7.4' + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jju@1.4.0: + resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + + jotai@2.18.1: + resolution: {integrity: sha512-e0NOzK+yRFwHo7DOp0DS0Ycq74KMEAObDWFGmfEL28PD9nLqBTt3/Ug7jf9ca72x0gC9LQZG9zH+0ISICmy3iA==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@babel/core': '>=7.0.0' + '@babel/template': '>=7.0.0' + '@types/react': '>=17.0.0' + react: '>=17.0.0' + peerDependenciesMeta: + '@babel/core': + optional: true + '@babel/template': + optional: true + '@types/react': + optional: true + react: + optional: true + + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + + js-file-download@0.4.12: + resolution: {integrity: sha512-rML+NkoD08p5Dllpjo0ffy4jRHeY6Zsapvr/W86N7E0yuzAO6qa5X9+xog6zQNlH102J7IXljNY2FtS6Lj3ucg==} + + js-sha3@0.8.0: + resolution: {integrity: sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsdom@26.1.0: + resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + jsonpath-plus@6.0.1: + resolution: {integrity: sha512-EvGovdvau6FyLexFH2OeXfIITlgIbgZoAZe3usiySeaIDm5QS+A10DKNpaPBBqqRSZr2HN6HVNXxtwUAr2apEw==} + engines: {node: '>=10.0.0'} + + keycharm@0.3.1: + resolution: {integrity: sha512-zn47Ti4FJT9zdF+YBBLWJsfKF/fYQHkrYlBeB5Ez5e2PjW7SoIxr43yehAne2HruulIoid4NKZZxO0dHBygCtQ==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kld-affine@2.1.1: + resolution: {integrity: sha512-NIS9sph8ZKdnQxZa5TcggaFs/Qr9zX3brFlGwE0+0Z4EzFIvAFuqLSwNeU4GkEpaX8ndh3ggGmWV7BPPcS3vjQ==} + engines: {node: '>= 10.15.3'} + + kld-intersections@0.7.0: + resolution: {integrity: sha512-/KuBU7Y5bRPGfc0yQ3QIoXPKqOQ6cBWDRl1XVMMa3pm4V6Ydbgy9e2fZoRxlSIU0gZSBt1c6gWLOzSGKbU8I3A==} + engines: {node: '>= 10.15.3'} + + kld-path-parser@0.2.1: + resolution: {integrity: sha512-C1EqY6vzqv5tdKeMF31L+JXq97n5zo67LiSEhZf4sPq8YeM+8ytp/qMGSKN8VdSPvFa6h1SR35aF4+T2JtxZww==} + engines: {node: '>= 10.15.3'} + + kld-polynomial@0.3.0: + resolution: {integrity: sha512-PEfxjQ6tsxL9DHBIhM2UZsSes0GI+OIMjbE0kj60jr80Biq/xXl1eGfnyzmfoackAMdKZtw2060L09HdjkPP5w==} + engines: {node: '>= 10.15.3'} + + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-script@1.0.0: + resolution: {integrity: sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==} + + local-pkg@1.1.2: + resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} + engines: {node: '>=14'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + + lowlight@1.20.0: + resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + luxon@3.7.2: + resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} + engines: {node: '>=12'} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + marked@14.0.0: + resolution: {integrity: sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==} + engines: {node: '>= 18'} + hasBin: true + + match-sorter@6.3.4: + resolution: {integrity: sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdast-util-directive@3.1.0: + resolution: {integrity: sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==} + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + memoize-one@5.2.1: + resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-directive@3.0.2: + resolution: {integrity: sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + + microseconds@0.2.0: + resolution: {integrity: sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + minim@0.23.8: + resolution: {integrity: sha512-bjdr2xW1dBCMsMGGsUeqM4eFI60m94+szhxWys+B1ztIt6gWSfeGBdSVCIawezeHYLYn0j6zrsXdQS/JllBzww==} + engines: {node: '>=6'} + + minimatch@10.2.3: + resolution: {integrity: sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg==} + engines: {node: 18 || 20 || >=22} + + minimatch@10.2.4: + resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mlly@1.8.1: + resolution: {integrity: sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==} + + mock-json-schema@1.1.2: + resolution: {integrity: sha512-3IyduYlhfzPy+nFN8wxUjloUi1hM7l8lN5LITuauUNMQltynJIOfLf/DADwTAp2d6kvSBtWojly1EuxX5B0WkA==} + + mock-property@1.0.3: + resolution: {integrity: sha512-2emPTb1reeLLYwHxyVx993iYyCHEiRRO+y8NFXFPL5kl5q14sgTK76cXyEKkeKCHeRw35SfdkUJ10Q1KfHuiIQ==} + engines: {node: '>= 0.4'} + + moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + + monaco-editor@0.55.1: + resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==} + + monaco-languages-jq@1.0.0: + resolution: {integrity: sha512-zOPxmPh2mEQs9GzGFR4KJIZ8IP4sWQZLIB1DYC5cchRkN1mcViFNf4a/XHO/vAwXD2X7kLDvYMpphFqikZLokQ==} + + mousetrap@1.6.5: + resolution: {integrity: sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + nano-time@1.0.0: + resolution: {integrity: sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + neotraverse@0.6.18: + resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==} + engines: {node: '>= 10'} + + next@16.1.6: + resolution: {integrity: sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==} + engines: {node: '>=20.9.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + + node-abort-controller@3.1.1: + resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-addon-api@8.6.0: + resolution: {integrity: sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==} + engines: {node: ^18 || ^20 || >= 21} + + node-cache@5.1.2: + resolution: {integrity: sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==} + engines: {node: '>= 8.0.0'} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch-commonjs@3.3.2: + resolution: {integrity: sha512-VBlAiynj3VMLrotgwOS3OyECFxas5y7ltLcK4t41lMUZeaK15Ym4QRkqN0EQKAFL42q9i21EPKjzLUPfltR72A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + + node-releases@2.0.36: + resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} + + nwsapi@2.2.23: + resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.12.3: + resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + oblivious-set@1.0.0: + resolution: {integrity: sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==} + + oblivious-set@1.1.1: + resolution: {integrity: sha512-Oh+8fK09mgGmAshFdH6hSVco6KZmd1tTwNFWj35OvzdmJTMZtAkbn05zar2iG3v6sDs1JLEtOiBGNb6BHwkb2w==} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + openapi-path-templating@2.2.1: + resolution: {integrity: sha512-eN14VrDvl/YyGxxrkGOHkVkWEoPyhyeydOUrbvjoz8K5eIGgELASwN1eqFOJ2CTQMGCy2EntOK1KdtJ8ZMekcg==} + engines: {node: '>=12.20.0'} + + openapi-server-url-templating@1.3.0: + resolution: {integrity: sha512-DPlCms3KKEbjVQb0spV6Awfn6UWNheuG/+folQPzh/wUaKwuqvj8zt5gagD7qoyxtE03cIiKPgLFS3Q8Bz00uQ==} + engines: {node: '>=12.20.0'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-cancelable@2.1.1: + resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} + engines: {node: '>=8'} + + p-cancelable@3.0.0: + resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} + engines: {node: '>=12.20'} + + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-queue@6.6.2: + resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} + engines: {node: '>=8'} + + p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-entities@2.0.0: + resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==} + + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse-ms@2.1.0: + resolution: {integrity: sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==} + engines: {node: '>=6'} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + + playwright-core@1.58.2: + resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.58.2: + resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} + engines: {node: '>=18'} + hasBin: true + + popper.js@1.16.1: + resolution: {integrity: sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==} + deprecated: You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1 + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.8.1: + resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} + engines: {node: '>=14'} + hasBin: true + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + prismjs@1.27.0: + resolution: {integrity: sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==} + engines: {node: '>=6'} + + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} + engines: {node: '>=6'} + + promise-polyfill@8.3.0: + resolution: {integrity: sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + propagating-hammerjs@1.5.0: + resolution: {integrity: sha512-3PUXWmomwutoZfydC+lJwK1bKCh6sK6jZGB31RUX6+4EXzsbkDZrK4/sVR7gBrvJaEIwpTVyxQUAd29FKkmVdw==} + + property-expr@2.0.6: + resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==} + + property-information@5.6.0: + resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==} + + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + pvtsutils@1.3.6: + resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==} + + pvutils@1.1.5: + resolution: {integrity: sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==} + engines: {node: '>=16.0.0'} + + q@1.4.1: + resolution: {integrity: sha512-/CdEdaw49VZVmyIDGUQKDDT53c7qBkO6g5CefWz91Ae+l4+cRtcDYwMTXh6me4O8TMldeGHG3N2Bl84V78Ywbg==} + engines: {node: '>=0.6.0', teleport: '>=0.2.0'} + deprecated: |- + You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other. + + (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) + + qs@6.15.0: + resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} + engines: {node: '>=0.6'} + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + query-state-core@3.1.0: + resolution: {integrity: sha512-92ZOQw7TW3yVZYk4V7K7CrRhvXTjbGW6nP9PshtY2yaGkixoGsbGi+DAgDaaxN7VaErYUmLjOmNmU64QQvg4mA==} + + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + + ramda-adjunct@5.1.0: + resolution: {integrity: sha512-8qCpl2vZBXEJyNbi4zqcgdfHtcdsWjOGbiNSEnEBrM6Y0OKOT8UxJbIVGm1TIcjaSu2MxaWcgtsNlKlCk7o7qg==} + engines: {node: '>=0.10.3'} + peerDependencies: + ramda: '>= 0.30.0' + + ramda@0.30.1: + resolution: {integrity: sha512-tEF5I22zJnuclswcZMc8bDIrwRHRzf+NqVEmqg50ShAZMP7MWeR/RGDthfM/p+BlqvF2fXAzpn8i+SJcYD3alw==} + + randexp@0.5.3: + resolution: {integrity: sha512-U+5l2KrcMNOUPYvazA3h5ekF80FHTUG+87SEAmHZmolh1M+i/WyTCxVzmi+tidIa1tM4BSe8g2Y/D3loWDjj+w==} + engines: {node: '>=4'} + + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + + rdk@6.6.3: + resolution: {integrity: sha512-+l6HyGiPDZnFMYci6/qv6cXxLEKiPrPPngAUV1iCBmtxMvEgMlhRi20x4SRAOwCUIsZDpjniibYbDOZ9/PfBcg==} + deprecated: 'deprecated: use reablocks instead' + peerDependencies: + react: '>=16' + react-dom: '>=16' + + react-confetti@6.4.0: + resolution: {integrity: sha512-5MdGUcqxrTU26I2EU7ltkWPwxvucQTuqMm8dUz72z2YMqTD6s9vMcDUysk7n9jnC+lXuCPeJJ7Knf98VEYE9Rg==} + engines: {node: '>=16'} + peerDependencies: + react: ^16.3.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 + + react-container-query@0.12.1: + resolution: {integrity: sha512-ObSKMpM/AcwnZk4oXZhApaw+wevpXLh7CM18wsZbUqJWzn+k5WKM1M5lV/crUZ2Ht8RnF5CTCnoi9et2Ji0j9w==} + peerDependencies: + react: ^0.14.0 || ^15.0.0-0 || ^16.0.0-0 || ^17 + react-dom: ^0.14.0 || ^15.0.0-0 || ^16.0.0-0 || ^17 + + react-cool-dimensions@2.0.7: + resolution: {integrity: sha512-z1VwkAAJ5d8QybDRuYIXTE41RxGr5GYsv1bQhbOBE8cMfoZQZpcF0odL64vdgrQVzat2jayedj1GoYi80FWcbA==} + peerDependencies: + react: '>= 16.8.0' + + react-copy-to-clipboard@5.1.0: + resolution: {integrity: sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==} + peerDependencies: + react: ^15.3.0 || 16 || 17 || 18 + + react-data-table-component@7.7.0: + resolution: {integrity: sha512-5knL6zMSKlbvzu9P04KM5Lx8/EyQujb4I9z3rWeoVX++IDJadQ7aR4X5J6EeS90wjK0Xoa6btaVeglnCAqD2ag==} + peerDependencies: + react: '>= 17.0.0' + styled-components: '>= 5.0.0' + + react-datepicker@6.9.0: + resolution: {integrity: sha512-QTxuzeem7BUfVFWv+g5WuvzT0c5BPo+XTCNbMTZKSZQLU+cMMwSUHwspaxuIcDlwNcOH0tiJ+bh1fJ2yxOGYWA==} + peerDependencies: + react: ^16.9.0 || ^17 || ^18 + react-dom: ^16.9.0 || ^17 || ^18 + + react-debounce-input@3.3.0: + resolution: {integrity: sha512-VEqkvs8JvY/IIZvh71Z0TC+mdbxERvYF33RcebnodlsUZ8RSgyKe2VWaHXv4+/8aoOgXLxWrdsYs2hDhcwbUgA==} + peerDependencies: + react: ^15.3.0 || 16 || 17 || 18 + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-fast-compare@3.2.2: + resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} + + react-helmet@6.1.0: + resolution: {integrity: sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==} + peerDependencies: + react: '>=16.3.0' + + react-highlight@0.15.0: + resolution: {integrity: sha512-5uV/b/N4Z421GSVVe05fz+OfTsJtFzx/fJBdafZyw4LS70XjIZwgEx3Lrkfc01W/RzZ2Dtfb0DApoaJFAIKBtA==} + + react-hook-form@7.71.2: + resolution: {integrity: sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + + react-hotkeys-hook@4.6.2: + resolution: {integrity: sha512-FmP+ZriY3EG59Ug/lxNfrObCnW9xQShgk7Nb83+CkpfkcCpfS95ydv+E9JuXA5cp8KtskU7LGlIARpkc92X22Q==} + peerDependencies: + react: '>=16.8.1' + react-dom: '>=16.8.1' + + react-immutable-proptypes@2.2.0: + resolution: {integrity: sha512-Vf4gBsePlwdGvSZoLSBfd4HAP93HDauMY4fDjXhreg/vg6F3Fj/MXDNyTbltPC/xZKmZc+cjLu3598DdYK6sgQ==} + peerDependencies: + immutable: '>=3.6.2' + + react-immutable-pure-component@2.2.2: + resolution: {integrity: sha512-vkgoMJUDqHZfXXnjVlG3keCxSO/U6WeDQ5/Sl0GK2cH8TOxEzQ5jXqDXHEL/jqk6fsNxV05oH5kD7VNMUE2k+A==} + peerDependencies: + immutable: '>= 2 || >= 4.0.0-rc' + react: '>= 16.6' + react-dom: '>= 16.6' + + react-inspector@6.0.2: + resolution: {integrity: sha512-x+b7LxhmHXjHoU/VrFAzw5iutsILRoYyDq97EDYdFpPLcvqtEzk4ZSZSQjnFPbr5T57tLXnHcqFYoN1pI6u8uQ==} + peerDependencies: + react: ^16.8.4 || ^17.0.0 || ^18.0.0 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react-is@19.2.4: + resolution: {integrity: sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==} + + react-markdown@10.1.0: + resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + + react-number-format@5.4.4: + resolution: {integrity: sha512-wOmoNZoOpvMminhifQYiYSTCLUDOiUbBunrMrMjA+dV52sY+vck1S4UhR6PkgnoCquvvMSeJjErXZ4qSaWCliA==} + peerDependencies: + react: ^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react-onclickoutside@6.13.2: + resolution: {integrity: sha512-h6Hbf1c8b7tIYY4u90mDdBLY4+AGQVMFtIE89HgC0DtVCh/JfKl477gYqUtGLmjZBKK3MJxomP/lFiLbz4sq9A==} + peerDependencies: + react: ^15.5.x || ^16.x || ^17.x || ^18.x + react-dom: ^15.5.x || ^16.x || ^17.x || ^18.x + + react-player@2.16.1: + resolution: {integrity: sha512-mxP6CqjSWjidtyDoMOSHVPdhX0pY16aSvw5fVr44EMaT7X5Xz46uQ4b/YBm1v2x+3hHkB9PmjEEkmbHb9PXQ4w==} + peerDependencies: + react: '>=16.6.0' + + react-query@3.39.3: + resolution: {integrity: sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + + react-redux@9.2.0: + resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} + peerDependencies: + '@types/react': ^18.2.25 || ^19 + react: ^18.0 || ^19 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-router-dom@7.13.1: + resolution: {integrity: sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router-use-location-state@3.1.2: + resolution: {integrity: sha512-qtpp9cPRxU2rPBt9EByzyvQ8XIUOA8kM94VuO4OZnio1HZlO0ANWfY8puYBT5Z/gKuycCK0/gqcxF94hF15nwQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.2 || ^18.0.0 + react-router: ^6.0.2 + + react-router@7.13.1: + resolution: {integrity: sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react-side-effect@2.1.2: + resolution: {integrity: sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==} + peerDependencies: + react: ^16.3.0 || ^17.0.0 || ^18.0.0 + + react-smooth@4.0.4: + resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react-syntax-highlighter@15.6.6: + resolution: {integrity: sha512-DgXrc+AZF47+HvAPEmn7Ua/1p10jNoVZVI/LoPiYdtY+OM+/nG5yefLHKJwdKqY1adMuHFbeyBaG9j64ML7vTw==} + peerDependencies: + react: '>= 0.14.0' + + react-syntax-highlighter@16.1.1: + resolution: {integrity: sha512-PjVawBGy80C6YbC5DDZJeUjBmC7skaoEUdvfFQediQHgCL7aKyVHe57SaJGfQsloGDac+gCpTfRdtxzWWKmCXA==} + engines: {node: '>= 16.20.2'} + peerDependencies: + react: '>= 0.14.0' + + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + + react-use-gesture@8.0.1: + resolution: {integrity: sha512-CXzUNkulUdgouaAlvAsC5ZVo0fi9KGSBSk81WrE4kOIcJccpANe9zZkAYr5YZZhqpicIFxitsrGVS4wmoMun9A==} + deprecated: This package is no longer maintained. Please use @use-gesture/react instead + peerDependencies: + react: '>= 16.8.0' + + react-vis-timeline@2.0.3: + resolution: {integrity: sha512-ltU3ZH005hErhe6tTU6/QAyIGLJwkka8sK5at/xyLLh/y3ZbJGaqmzFPcal1+zkEtGJe2JL0EUGLSGzNFkhPBQ==} + peerDependencies: + lodash: ^4.17.15 + moment: ^2.25 + react: ^0.14 || ^15.0 || ^16.0 + react-dom: ^0.14 || ^15.0 || ^16.0 + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + reaflow@5.1.2: + resolution: {integrity: sha512-8DctXn+sudiITeOmr5/AbALjVe3IBOzCvKdT9VZydXxMp0xbJiWBKiO8duFktPnYL293zOBTmgLjm7CcJXG32w==} + peerDependencies: + react: '>=16' + react-dom: '>=16' + + reakeys@1.3.1: + resolution: {integrity: sha512-k75rJxIiNtA9B6a9ijgj3n7CJKhdY9hctFzac5IBnMKEzk5RpwHtOZON1xqP9fQQAscpa922lbkUMW8q94M0fg==} + peerDependencies: + react: '>=16' + react-dom: '>=16' + + recharts-scale@0.4.5: + resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} + + recharts@2.15.4: + resolution: {integrity: sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==} + engines: {node: '>=14'} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + redux-immutable@4.0.0: + resolution: {integrity: sha512-SchSn/DWfGb3oAejd+1hhHx01xUoxY+V7TeK0BKqpkLKiQPVFf7DYzEaKmrEVxsWxielKfSK9/Xq66YyxgR1cg==} + peerDependencies: + immutable: ^3.8.1 || ^4.0.0-rc.1 + + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + refractor@3.6.0: + resolution: {integrity: sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==} + + refractor@5.0.0: + resolution: {integrity: sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + remark-directive@3.0.1: + resolution: {integrity: sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A==} + + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + remarkable@2.0.1: + resolution: {integrity: sha512-YJyMcOH5lrR+kZdmB0aJJ4+93bEojRZ1HGDn9Eagu6ibg7aVZhc3OWbbShRid+Q5eAfsEqWxpe+g5W5nYNfNiA==} + engines: {node: '>= 6.0.0'} + hasBin: true + + remove-accents@0.4.4: + resolution: {integrity: sha512-EpFcOa/ISetVHEXqu+VwI96KZBmq+a8LJnGkaeFw45epGlxIZz5dhEEnNZMsQXgORu3qaMoLX4qJCzOik6ytAg==} + + remove-accents@0.5.0: + resolution: {integrity: sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==} + + repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + + resize-observer-lite@0.2.3: + resolution: {integrity: sha512-k/p+pjCTQkQ7x94bWsxcVwEJI5SrcO95j7czrCKMpHjXFQ+HmKRGLTdAkZoL3+wG1Pe/4L9Sl652zy9lU54dFg==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + ret@0.2.2: + resolution: {integrity: sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==} + engines: {node: '>=4'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sass@1.97.3: + resolution: {integrity: sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==} + engines: {node: '>=14.0.0'} + hasBin: true + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + select@1.1.2: + resolution: {integrity: sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + serialize-error@8.1.0: + resolution: {integrity: sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==} + engines: {node: '>=10'} + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + sha.js@2.4.12: + resolution: {integrity: sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==} + engines: {node: '>= 0.10'} + hasBin: true + + shallowequal@1.1.0: + resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + short-unique-id@5.3.2: + resolution: {integrity: sha512-KRT/hufMSxXKEDSQujfVE0Faa/kZ51ihUcZQAcmP04t00DvPj7Ox5anHke1sJYUtzSuiT/Y5uyzg/W7bBEGhCg==} + hasBin: true + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + simple-swizzle@0.2.4: + resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} + + snake-case@3.0.4: + resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + space-separated-tokens@1.1.5: + resolution: {integrity: sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + stable-hash@0.0.5: + resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + state-local@1.0.7: + resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + + string-ts@2.3.1: + resolution: {integrity: sha512-xSJq+BS52SaFFAVxuStmx6n5aYZU571uYUnUrPXkPFCfdHyZMMlbP2v2Wx5sNBnAVzq/2+0+mcBLBa3Xa5ubYw==} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + + styled-components@5.3.11: + resolution: {integrity: sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw==} + engines: {node: '>=10'} + peerDependencies: + react: '>= 16.8.0' + react-dom: '>= 16.8.0' + react-is: '>= 16.8.0' + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + stylis@4.2.0: + resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + svg-parser@2.0.4: + resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} + + swagger-client@3.37.0: + resolution: {integrity: sha512-pzU+B+DkUbrSwlj4/E8sGeP1w84/CFgDJAt80fHu650TxnOHbqFLGQjiE6luvpRxTPdfK2zRHJP7I6CgUkI8yA==} + + swagger-ui-react@5.32.0: + resolution: {integrity: sha512-2mmrtvfp0EA90pdT8qXTMu26ex03TG2bsjvDAwXhdfCm+9foyadYJN+nEvDHM6/c6/xtXbdAsb6cVxBvbltnpw==} + peerDependencies: + react: '>=16.8.0 <20' + react-dom: '>=16.8.0 <20' + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + tabbable@6.4.0: + resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} + + tape@4.17.0: + resolution: {integrity: sha512-KCuXjYxCZ3ru40dmND+oCLsXyuA8hoseu2SS404Px5ouyS0A99v8X/mdiLqsR5MTAyamMBN7PRwt2Dv3+xGIxw==} + hasBin: true + + text-encoding@0.7.0: + resolution: {integrity: sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA==} + deprecated: no longer maintained + + tiny-case@1.0.3: + resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} + + tiny-emitter@1.1.0: + resolution: {integrity: sha512-HFhr+OKGIHRO6krgzEt9MqbMO98wPDzDPr1BOpM/nZCChkK40UYn8b70nSjcan4jTzDSQecy1KRVVQRohIRWrw==} + + tiny-emitter@2.1.0: + resolution: {integrity: sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + + to-buffer@1.2.2: + resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==} + engines: {node: '>= 0.4'} + + toggle-selection@1.0.6: + resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==} + + toposort@2.0.2: + resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==} + + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + + tree-sitter-json@0.24.8: + resolution: {integrity: sha512-Tc9ZZYwHyWZ3Tt1VEw7Pa2scu1YO7/d2BCBbKTx5hXwig3UfdQjsOPkPyLpDJOn/m1UBEWYAtSdGAwCSyagBqQ==} + peerDependencies: + tree-sitter: ^0.21.1 + peerDependenciesMeta: + tree-sitter: + optional: true + + tree-sitter@0.21.1: + resolution: {integrity: sha512-7dxoA6kYvtgWw80265MyqJlkRl4yawIjO7S5MigytjELkX43fV2WsAXzsNfO7sBpPPCF5Gp0+XzHk0DwLCq3xQ==} + + tree-sitter@0.22.4: + resolution: {integrity: sha512-usbHZP9/oxNsUY65MQUsduGRqDHQOou1cagUSwjhoSYAmSahjQDAVsh9s+SlZkn8X8+O1FULRGwHu7AFP3kjzg==} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-declaration-location@1.0.7: + resolution: {integrity: sha512-EDyGAwH1gO0Ausm9gV6T2nUvBgXT5kGoCMJPllOaooZ+4VvJiKBdZE7wK18N1deEowhcUptS+5GXZK8U/fvpwA==} + peerDependencies: + typescript: '>=4.0.0' + + ts-key-enum@2.0.13: + resolution: {integrity: sha512-zixs6j8+NhzazLUQ1SiFrlo1EFWG/DbqLuUGcWWZ5zhwjRT7kbi1hBlofxdqel+h28zrby2It5TrOyKp04kvqw==} + + ts-mixer@6.0.4: + resolution: {integrity: sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==} + + ts-pattern@5.9.0: + resolution: {integrity: sha512-6s5V71mX8qBUmlgbrfL33xDUwO0fq48rxAu2LBE11WBeGdpCPOsXksQbZJHvHwhrd3QjUusd3mAOM5Gg0mFBLg==} + + ts-toolbelt@9.6.0: + resolution: {integrity: sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==} + + tsconfck@3.1.6: + resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} + engines: {node: ^18 || >=20} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tslib@2.4.0: + resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tween-functions@1.2.0: + resolution: {integrity: sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + types-ramda@0.30.1: + resolution: {integrity: sha512-1HTsf5/QVRmLzcGfldPFvkVsAdi1db1BBKzi7iW3KBUlOICg/nKnFS+jGqDJS3YD8VsWbAh7JiHeBvbsw8RPxA==} + + typescript-eslint@8.57.0: + resolution: {integrity: sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + typescript@5.8.2: + resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} + engines: {node: '>=14.17'} + hasBin: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + u2f-api-polyfill@0.4.3: + resolution: {integrity: sha512-0DVykdzG3tKft2GciQCGzgO8BinDEfIhTBo7FKbLBmA+sVTPYmNOFbsZuduYQmnc3+ykUadTHNqXVqnvBfLCvg==} + + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + underscore@1.13.1: + resolution: {integrity: sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==} + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + undici@7.22.0: + resolution: {integrity: sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==} + engines: {node: '>=20.18.1'} + + undoo@0.5.0: + resolution: {integrity: sha512-SPlDcde+AUHoFKeVlH2uBJxqVkw658I4WR2rPoygC1eRCzm3GeoP8S6xXZVJeBVOQQid8X2xUBW0N4tOvvHH3Q==} + + unfetch@4.2.0: + resolution: {integrity: sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==} + + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unload@2.2.0: + resolution: {integrity: sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==} + + unload@2.3.1: + resolution: {integrity: sha512-MUZEiDqvAN9AIDRbbBnVYVvfcR6DrjCqeU2YQMmliFZl9uaBUjTkhuDQkBiyAy8ad5bx1TXVbqZ3gg7namsWjA==} + + unraw@3.0.0: + resolution: {integrity: sha512-08/DA66UF65OlpUDIQtbJyrqTR0jTAlJ+jsnkQ4jxR7+K5g5YG1APZKQSMCE1vqqmD+2pv6+IdEjmopFatacvg==} + + unrs-resolver@1.11.1: + resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + + use-isomorphic-layout-effect@1.2.1: + resolution: {integrity: sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + use-location-state@3.1.2: + resolution: {integrity: sha512-zHZMRkFhswmMuEnSaqYKfTT13i7WIPJWyVGt+/wiVW8lna5MaLIq0uV9+oG5ZJ+oG1BooO0NsE2lJHJfzbfZpA==} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.2 || ^18.0.0 + next: '*' + react: ^16.8.0 || ^17.0.2 || ^18.0.0 + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + uuid@7.0.3: + resolution: {integrity: sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==} + hasBin: true + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + victory-vendor@36.9.2: + resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + + vis-data@6.6.1: + resolution: {integrity: sha512-xmujDB2Dzf8T04rGFJ9OP4OA6zRVrz8R9hb0CVKryBrZRCljCga9JjSfgctA8S7wdZu7otDtUIwX4ZOgfV/57w==} + peerDependencies: + moment: ^2.24.0 + uuid: ^7.0.0 || ^8.0.0 + vis-util: ^4.0.0 + + vis-timeline@7.3.6: + resolution: {integrity: sha512-ddmAvHWGcIwIn7tTgR/5h2cNXbPtfRR5MWyO1njJYZTAo9fh+2WjnrV3K05qHLBH0gCQTXvO3ue/DeLPEl7dkw==} + peerDependencies: + '@egjs/hammerjs': ^2.0.0 + component-emitter: ^1.3.0 + keycharm: ^0.3.0 + moment: ^2.24.0 + propagating-hammerjs: ^1.4.0 + uuid: ^3.4.0 || ^7.0.0 + vis-data: ^6.3.0 + vis-util: ^3.0.0 || ^4.0.0 + + vis-util@4.3.4: + resolution: {integrity: sha512-hJIZNrwf4ML7FYjs+m+zjJfaNvhjk3/1hbMdQZVnwwpOFJS/8dMG8rdbOHXcKoIEM6U5VOh3HNpaDXxGkOZGpw==} + engines: {node: '>=8'} + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite-plugin-dts@4.5.4: + resolution: {integrity: sha512-d4sOM8M/8z7vRXHHq/ebbblfaxENjogAAekcfcDCCwAyvGqnPrc7f4NZbvItS+g4WTgerW0xDwSz5qz11JT3vg==} + peerDependencies: + typescript: '*' + vite: '*' + peerDependenciesMeta: + vite: + optional: true + + vite-plugin-svgr@4.5.0: + resolution: {integrity: sha512-W+uoSpmVkSmNOGPSsDCWVW/DDAyv+9fap9AZXBvWiQqrboJ08j2vh0tFxTD/LjwqwAd3yYSVJgm54S/1GhbdnA==} + peerDependencies: + vite: '>=2.6.0' + + vite-tsconfig-paths@5.1.4: + resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} + peerDependencies: + vite: '*' + peerDependenciesMeta: + vite: + optional: true + + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + web-tree-sitter@0.24.5: + resolution: {integrity: sha512-+J/2VSHN8J47gQUAvF8KDadrfz6uFYVjxoxbKWDoXVsH2u7yLdarCnIURnrMA6uSRkgX3SdmqM5BOoQjPdSh5w==} + + webcrypto-core@1.8.1: + resolution: {integrity: sha512-P+x1MvlNCXlKbLSOY4cYrdreqPG5hbzkmawbcXLKN/mf6DZW0SdNNkZ+sjwsqVkI4A4Ko2sPZmkZtCKY58w83A==} + + webcrypto-shim@0.1.7: + resolution: {integrity: sha512-JAvAQR5mRNRxZW2jKigWMjCMkjSdmP5cColRP1U/pTg69VgHXEi1orv5vVpJ55Zc5MIaPc1aaurzd9pjv2bveg==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.20: + resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xhr2@0.1.3: + resolution: {integrity: sha512-6RmGK22QwC7yXB1CRwyLWuS2opPcKOlAu0ViAnyZjDlzrEmCKL4kLHkfvB8oMRWeztMsNoDGAjsMZY15w/4tTw==} + engines: {node: '>= 0.6'} + + xml-but-prettier@1.0.1: + resolution: {integrity: sha512-C2CJaadHrZTqESlH03WOyw0oZTtoy2uEg6dSDF6YRg+9GnYNub53RRemLpnvtbHDFelxMx4LajiFsYeR6XJHgQ==} + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xml@1.0.1: + resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + xstate@4.38.3: + resolution: {integrity: sha512-SH7nAaaPQx57dx6qvfcIgqKRXIh4L0A1iYEqim4s1u7c9VoCgzZc+63FY90AKU4ZzOC2cfJzTnpO4zK7fCUzzw==} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yup@1.7.1: + resolution: {integrity: sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw==} + + zenscroll@4.0.2: + resolution: {integrity: sha512-jEA1znR7b4C/NnaycInCU6h/d15ZzCd1jmsruqOKnZP6WXQSMH3W2GL+OXbkruslU4h+Tzuos0HdswzRUk/Vgg==} + + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@adobe/css-tools@4.4.4': {} + + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + + '@auth0/auth0-react@1.12.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@auth0/auth0-spa-js': 1.22.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@auth0/auth0-spa-js@1.22.6': + dependencies: + abortcontroller-polyfill: 1.7.8 + browser-tabs-lock: 1.3.0 + core-js: 3.48.0 + es-cookie: 1.3.2 + fast-text-encoding: 1.0.6 + promise-polyfill: 8.3.0 + unfetch: 4.2.0 + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0(supports-color@5.5.0) + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3(supports-color@5.5.0) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6(supports-color@5.5.0)': + dependencies: + '@babel/traverse': 7.29.0(supports-color@5.5.0) + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6(supports-color@5.5.0) + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/runtime-corejs3@7.29.0': + dependencies: + core-js-pure: 3.48.0 + + '@babel/runtime@7.28.6': {} + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0(supports-color@5.5.0)': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@csstools/color-helpers@5.1.0': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-tokenizer@3.0.4': {} + + '@datasert/cronjs-matcher@1.4.0': + dependencies: + '@datasert/cronjs-parser': 1.4.0 + luxon: 3.7.2 + + '@datasert/cronjs-parser@1.4.0': {} + + '@date-io/core@3.2.0': {} + + '@date-io/dayjs@3.2.0(dayjs@1.10.7)': + dependencies: + '@date-io/core': 3.2.0 + optionalDependencies: + dayjs: 1.10.7 + + '@dnd-kit/accessibility@3.1.1(react@18.3.1)': + dependencies: + react: 18.3.1 + tslib: 2.8.1 + + '@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@dnd-kit/accessibility': 3.1.1(react@18.3.1) + '@dnd-kit/utilities': 3.2.2(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.8.1 + + '@dnd-kit/sortable@8.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@dnd-kit/utilities': 3.2.2(react@18.3.1) + react: 18.3.1 + tslib: 2.8.1 + + '@dnd-kit/utilities@3.2.2(react@18.3.1)': + dependencies: + react: 18.3.1 + tslib: 2.8.1 + + '@egjs/hammerjs@2.0.17': + dependencies: + '@types/hammerjs': 2.0.46 + + '@emnapi/core@1.8.1': + dependencies: + '@emnapi/wasi-threads': 1.1.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.8.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.1.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emotion/babel-plugin@11.13.5': + dependencies: + '@babel/helper-module-imports': 7.28.6(supports-color@5.5.0) + '@babel/runtime': 7.28.6 + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/serialize': 1.3.3 + babel-plugin-macros: 3.1.0 + convert-source-map: 1.9.0 + escape-string-regexp: 4.0.0 + find-root: 1.1.0 + source-map: 0.5.7 + stylis: 4.2.0 + transitivePeerDependencies: + - supports-color + + '@emotion/cache@11.14.0': + dependencies: + '@emotion/memoize': 0.9.0 + '@emotion/sheet': 1.4.0 + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + stylis: 4.2.0 + + '@emotion/hash@0.9.2': {} + + '@emotion/is-prop-valid@0.8.8': + dependencies: + '@emotion/memoize': 0.7.4 + optional: true + + '@emotion/is-prop-valid@1.4.0': + dependencies: + '@emotion/memoize': 0.9.0 + + '@emotion/memoize@0.7.4': + optional: true + + '@emotion/memoize@0.9.0': {} + + '@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.6 + '@emotion/babel-plugin': 11.13.5 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@18.3.1) + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + hoist-non-react-statics: 3.3.2 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + transitivePeerDependencies: + - supports-color + + '@emotion/serialize@1.3.3': + dependencies: + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/unitless': 0.10.0 + '@emotion/utils': 1.4.2 + csstype: 3.2.3 + + '@emotion/sheet@1.4.0': {} + + '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.6 + '@emotion/babel-plugin': 11.13.5 + '@emotion/is-prop-valid': 1.4.0 + '@emotion/react': 11.14.0(@types/react@18.3.28)(react@18.3.1) + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@18.3.1) + '@emotion/utils': 1.4.2 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + transitivePeerDependencies: + - supports-color + + '@emotion/stylis@0.8.5': {} + + '@emotion/unitless@0.10.0': {} + + '@emotion/unitless@0.7.5': {} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@emotion/utils@1.4.2': {} + + '@emotion/weak-memoize@0.4.0': {} + + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.27.3': + optional: true + + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-x64@0.27.3': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.32.0)': + dependencies: + eslint: 9.32.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint-react/ast@1.53.1(eslint@9.32.0)(typescript@5.9.3)': + dependencies: + '@eslint-react/eff': 1.53.1 + '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/typescript-estree': 8.57.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.0(eslint@9.32.0)(typescript@5.9.3) + string-ts: 2.3.1 + ts-pattern: 5.9.0 + transitivePeerDependencies: + - eslint + - supports-color + - typescript + + '@eslint-react/core@1.53.1(eslint@9.32.0)(typescript@5.9.3)': + dependencies: + '@eslint-react/ast': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@eslint-react/eff': 1.53.1 + '@eslint-react/kit': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@eslint-react/shared': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@eslint-react/var': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.57.0 + '@typescript-eslint/type-utils': 8.57.0(eslint@9.32.0)(typescript@5.9.3) + '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/utils': 8.57.0(eslint@9.32.0)(typescript@5.9.3) + birecord: 0.1.1 + ts-pattern: 5.9.0 + transitivePeerDependencies: + - eslint + - supports-color + - typescript + + '@eslint-react/eff@1.53.1': {} + + '@eslint-react/eslint-plugin@1.53.1(eslint@9.32.0)(ts-api-utils@2.4.0(typescript@5.9.3))(typescript@5.9.3)': + dependencies: + '@eslint-react/eff': 1.53.1 + '@eslint-react/kit': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@eslint-react/shared': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.57.0 + '@typescript-eslint/type-utils': 8.57.0(eslint@9.32.0)(typescript@5.9.3) + '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/utils': 8.57.0(eslint@9.32.0)(typescript@5.9.3) + eslint: 9.32.0 + eslint-plugin-react-debug: 1.53.1(eslint@9.32.0)(typescript@5.9.3) + eslint-plugin-react-dom: 1.53.1(eslint@9.32.0)(typescript@5.9.3) + eslint-plugin-react-hooks-extra: 1.53.1(eslint@9.32.0)(typescript@5.9.3) + eslint-plugin-react-naming-convention: 1.53.1(eslint@9.32.0)(typescript@5.9.3) + eslint-plugin-react-web-api: 1.53.1(eslint@9.32.0)(typescript@5.9.3) + eslint-plugin-react-x: 1.53.1(eslint@9.32.0)(ts-api-utils@2.4.0(typescript@5.9.3))(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + - ts-api-utils + + '@eslint-react/kit@1.53.1(eslint@9.32.0)(typescript@5.9.3)': + dependencies: + '@eslint-react/eff': 1.53.1 + '@typescript-eslint/utils': 8.57.0(eslint@9.32.0)(typescript@5.9.3) + ts-pattern: 5.9.0 + zod: 4.3.6 + transitivePeerDependencies: + - eslint + - supports-color + - typescript + + '@eslint-react/shared@1.53.1(eslint@9.32.0)(typescript@5.9.3)': + dependencies: + '@eslint-react/eff': 1.53.1 + '@eslint-react/kit': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.0(eslint@9.32.0)(typescript@5.9.3) + ts-pattern: 5.9.0 + zod: 4.3.6 + transitivePeerDependencies: + - eslint + - supports-color + - typescript + + '@eslint-react/var@1.53.1(eslint@9.32.0)(typescript@5.9.3)': + dependencies: + '@eslint-react/ast': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@eslint-react/eff': 1.53.1 + '@typescript-eslint/scope-manager': 8.57.0 + '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/utils': 8.57.0(eslint@9.32.0)(typescript@5.9.3) + string-ts: 2.3.1 + ts-pattern: 5.9.0 + transitivePeerDependencies: + - eslint + - supports-color + - typescript + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3(supports-color@5.5.0) + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.3.1': {} + + '@eslint/core@0.15.2': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.5': + dependencies: + ajv: 6.14.0 + debug: 4.4.3(supports-color@5.5.0) + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.32.0': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.3.5': + dependencies: + '@eslint/core': 0.15.2 + levn: 0.4.1 + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/react-dom@2.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/dom': 1.7.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@floating-ui/react@0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@floating-ui/utils': 0.2.11 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tabbable: 6.4.0 + + '@floating-ui/utils@0.2.11': {} + + '@growthbook/growthbook-react@1.6.5(react@18.3.1)': + dependencies: + '@growthbook/growthbook': 1.6.5 + react: 18.3.1 + + '@growthbook/growthbook@1.6.5': + dependencies: + dom-mutator: 0.6.0 + + '@hookform/resolvers@5.2.2(react-hook-form@7.71.2(react@18.3.1))': + dependencies: + '@standard-schema/utils': 0.3.0 + react-hook-form: 7.71.2(react@18.3.1) + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@img/colour@1.1.0': + optional: true + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.8.1 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@io-orkes/conductor-javascript@2.4.1': + optionalDependencies: + undici: 7.22.0 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@jsonforms/core@3.7.0': + dependencies: + '@types/json-schema': 7.0.15 + ajv: 8.18.0 + ajv-formats: 2.1.1(ajv@8.18.0) + lodash: 4.17.23 + + '@jsonforms/material-renderers@3.7.0(9e4c19b25267976d34663ea61a4c9150)': + dependencies: + '@date-io/dayjs': 3.2.0(dayjs@1.10.7) + '@emotion/react': 11.14.0(@types/react@18.3.28)(react@18.3.1) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1) + '@jsonforms/core': 3.7.0 + '@jsonforms/react': 3.7.0(@jsonforms/core@3.7.0)(react@18.3.1) + '@mui/icons-material': 7.3.9(@mui/material@7.3.9(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.28)(react@18.3.1) + '@mui/material': 7.3.9(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@mui/x-date-pickers': 6.20.2(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@mui/material@7.3.9(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@7.3.9(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(date-fns@2.30.0)(dayjs@1.10.7)(luxon@3.7.2)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + dayjs: 1.10.7 + lodash: 4.17.23 + react: 18.3.1 + + '@jsonforms/react@3.7.0(@jsonforms/core@3.7.0)(react@18.3.1)': + dependencies: + '@jsonforms/core': 3.7.0 + lodash: 4.17.23 + react: 18.3.1 + + '@ljharb/resumer@0.0.1': + dependencies: + '@ljharb/through': 2.3.14 + + '@ljharb/through@2.3.14': + dependencies: + call-bind: 1.0.8 + + '@microsoft/api-extractor-model@7.33.4(@types/node@24.12.0)': + dependencies: + '@microsoft/tsdoc': 0.16.0 + '@microsoft/tsdoc-config': 0.18.1 + '@rushstack/node-core-library': 5.20.3(@types/node@24.12.0) + transitivePeerDependencies: + - '@types/node' + + '@microsoft/api-extractor@7.57.7(@types/node@24.12.0)': + dependencies: + '@microsoft/api-extractor-model': 7.33.4(@types/node@24.12.0) + '@microsoft/tsdoc': 0.16.0 + '@microsoft/tsdoc-config': 0.18.1 + '@rushstack/node-core-library': 5.20.3(@types/node@24.12.0) + '@rushstack/rig-package': 0.7.2 + '@rushstack/terminal': 0.22.3(@types/node@24.12.0) + '@rushstack/ts-command-line': 5.3.3(@types/node@24.12.0) + diff: 8.0.3 + lodash: 4.17.23 + minimatch: 10.2.3 + resolve: 1.22.11 + semver: 7.5.4 + source-map: 0.6.1 + typescript: 5.8.2 + transitivePeerDependencies: + - '@types/node' + + '@microsoft/tsdoc-config@0.18.1': + dependencies: + '@microsoft/tsdoc': 0.16.0 + ajv: 8.18.0 + jju: 1.4.0 + resolve: 1.22.11 + + '@microsoft/tsdoc@0.16.0': {} + + '@monaco-editor/loader@1.7.0': + dependencies: + state-local: 1.0.7 + + '@monaco-editor/react@4.7.0(monaco-editor@0.55.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@monaco-editor/loader': 1.7.0 + monaco-editor: 0.55.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@motionone/animation@10.18.0': + dependencies: + '@motionone/easing': 10.18.0 + '@motionone/types': 10.17.1 + '@motionone/utils': 10.18.0 + tslib: 2.4.0 + + '@motionone/dom@10.18.0': + dependencies: + '@motionone/animation': 10.18.0 + '@motionone/generators': 10.18.0 + '@motionone/types': 10.17.1 + '@motionone/utils': 10.18.0 + hey-listen: 1.0.8 + tslib: 2.4.0 + + '@motionone/easing@10.18.0': + dependencies: + '@motionone/utils': 10.18.0 + tslib: 2.4.0 + + '@motionone/generators@10.18.0': + dependencies: + '@motionone/types': 10.17.1 + '@motionone/utils': 10.18.0 + tslib: 2.4.0 + + '@motionone/types@10.17.1': {} + + '@motionone/utils@10.18.0': + dependencies: + '@motionone/types': 10.17.1 + hey-listen: 1.0.8 + tslib: 2.4.0 + + '@mui/base@5.0.0-beta.70(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.6 + '@floating-ui/react-dom': 2.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@mui/types': 7.2.24(@types/react@18.3.28) + '@mui/utils': 6.4.9(@types/react@18.3.28)(react@18.3.1) + '@popperjs/core': 2.11.8 + clsx: 2.1.1 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + + '@mui/core-downloads-tracker@7.3.9': {} + + '@mui/icons-material@7.3.9(@mui/material@7.3.9(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.6 + '@mui/material': 7.3.9(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@mui/material@7.3.9(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.6 + '@mui/core-downloads-tracker': 7.3.9 + '@mui/system': 7.3.9(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1) + '@mui/types': 7.4.12(@types/react@18.3.28) + '@mui/utils': 7.3.9(@types/react@18.3.28)(react@18.3.1) + '@popperjs/core': 2.11.8 + '@types/react-transition-group': 4.4.12(@types/react@18.3.28) + clsx: 2.1.1 + csstype: 3.2.3 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-is: 19.2.4 + react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@18.3.28)(react@18.3.1) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1) + '@types/react': 18.3.28 + + '@mui/private-theming@7.3.9(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.6 + '@mui/utils': 7.3.9(@types/react@18.3.28)(react@18.3.1) + prop-types: 15.8.1 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@mui/styled-engine@7.3.9(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.6 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/sheet': 1.4.0 + csstype: 3.2.3 + prop-types: 15.8.1 + react: 18.3.1 + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@18.3.28)(react@18.3.1) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1) + + '@mui/system@7.3.9(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.6 + '@mui/private-theming': 7.3.9(@types/react@18.3.28)(react@18.3.1) + '@mui/styled-engine': 7.3.9(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(react@18.3.1) + '@mui/types': 7.4.12(@types/react@18.3.28) + '@mui/utils': 7.3.9(@types/react@18.3.28)(react@18.3.1) + clsx: 2.1.1 + csstype: 3.2.3 + prop-types: 15.8.1 + react: 18.3.1 + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@18.3.28)(react@18.3.1) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1) + '@types/react': 18.3.28 + + '@mui/types@7.2.24(@types/react@18.3.28)': + optionalDependencies: + '@types/react': 18.3.28 + + '@mui/types@7.4.12(@types/react@18.3.28)': + dependencies: + '@babel/runtime': 7.28.6 + optionalDependencies: + '@types/react': 18.3.28 + + '@mui/utils@5.17.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.6 + '@mui/types': 7.2.24(@types/react@18.3.28) + '@types/prop-types': 15.7.15 + clsx: 2.1.1 + prop-types: 15.8.1 + react: 18.3.1 + react-is: 19.2.4 + optionalDependencies: + '@types/react': 18.3.28 + + '@mui/utils@6.4.9(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.6 + '@mui/types': 7.2.24(@types/react@18.3.28) + '@types/prop-types': 15.7.15 + clsx: 2.1.1 + prop-types: 15.8.1 + react: 18.3.1 + react-is: 19.2.4 + optionalDependencies: + '@types/react': 18.3.28 + + '@mui/utils@7.3.9(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.6 + '@mui/types': 7.4.12(@types/react@18.3.28) + '@types/prop-types': 15.7.15 + clsx: 2.1.1 + prop-types: 15.8.1 + react: 18.3.1 + react-is: 19.2.4 + optionalDependencies: + '@types/react': 18.3.28 + + '@mui/x-date-pickers@6.20.2(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@mui/material@7.3.9(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@7.3.9(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(date-fns@2.30.0)(dayjs@1.10.7)(luxon@3.7.2)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.6 + '@mui/base': 5.0.0-beta.70(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@mui/material': 7.3.9(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@mui/system': 7.3.9(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1) + '@mui/utils': 5.17.1(@types/react@18.3.28)(react@18.3.1) + '@types/react-transition-group': 4.4.12(@types/react@18.3.28) + clsx: 2.1.1 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@18.3.28)(react@18.3.1) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1) + date-fns: 2.30.0 + dayjs: 1.10.7 + luxon: 3.7.2 + moment: 2.30.1 + transitivePeerDependencies: + - '@types/react' + + '@napi-rs/wasm-runtime@0.2.12': + dependencies: + '@emnapi/core': 1.8.1 + '@emnapi/runtime': 1.8.1 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@next/env@16.1.6': {} + + '@next/swc-darwin-arm64@16.1.6': + optional: true + + '@next/swc-darwin-x64@16.1.6': + optional: true + + '@next/swc-linux-arm64-gnu@16.1.6': + optional: true + + '@next/swc-linux-arm64-musl@16.1.6': + optional: true + + '@next/swc-linux-x64-gnu@16.1.6': + optional: true + + '@next/swc-linux-x64-musl@16.1.6': + optional: true + + '@next/swc-win32-arm64-msvc@16.1.6': + optional: true + + '@next/swc-win32-x64-msvc@16.1.6': + optional: true + + '@nolyfill/is-core-module@1.0.39': {} + + '@okta/okta-auth-js@6.9.0': + dependencies: + '@babel/runtime': 7.28.6 + '@babel/runtime-corejs3': 7.29.0 + '@peculiar/webcrypto': 1.5.0 + Base64: 1.1.0 + atob: 2.1.2 + broadcast-channel: 4.17.0 + btoa: 1.2.1 + core-js: 3.48.0 + cross-fetch: 3.2.0 + js-cookie: 3.0.5 + jsonpath-plus: 6.0.1 + node-cache: 5.1.2 + p-cancelable: 2.1.1 + text-encoding: 0.7.0 + tiny-emitter: 1.1.0 + webcrypto-shim: 0.1.7 + xhr2: 0.1.3 + transitivePeerDependencies: + - encoding + + '@okta/okta-react@6.10.0(@okta/okta-auth-js@6.9.0)(react-dom@18.3.1(react@18.3.1))(react-router-dom@7.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.6 + '@okta/okta-auth-js': 6.9.0 + compare-versions: 4.1.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-router-dom: 7.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + '@okta/okta-signin-widget@6.9.0': + dependencies: + '@okta/okta-auth-js': 6.9.0 + '@sindresorhus/to-milliseconds': 1.2.0 + '@types/backbone': 1.4.23 + '@types/jquery': 3.5.34 + '@types/jqueryui': 1.12.24 + '@types/q': 1.5.8 + '@types/selectize': 0.12.39 + '@types/underscore': 1.13.0 + clipboard: 1.7.1 + cross-fetch: 3.2.0 + handlebars: 4.7.8 + parse-ms: 2.1.0 + q: 1.4.1 + u2f-api-polyfill: 0.4.3 + underscore: 1.13.1 + optionalDependencies: + fsevents: 2.3.3 + transitivePeerDependencies: + - encoding + + '@parcel/watcher-android-arm64@2.5.6': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.6': + optional: true + + '@parcel/watcher-darwin-x64@2.5.6': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.6': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.6': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.6': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.6': + optional: true + + '@parcel/watcher-win32-arm64@2.5.6': + optional: true + + '@parcel/watcher-win32-ia32@2.5.6': + optional: true + + '@parcel/watcher-win32-x64@2.5.6': + optional: true + + '@parcel/watcher@2.5.6': + dependencies: + detect-libc: 2.1.2 + is-glob: 4.0.3 + node-addon-api: 7.1.1 + picomatch: 4.0.3 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.6 + '@parcel/watcher-darwin-arm64': 2.5.6 + '@parcel/watcher-darwin-x64': 2.5.6 + '@parcel/watcher-freebsd-x64': 2.5.6 + '@parcel/watcher-linux-arm-glibc': 2.5.6 + '@parcel/watcher-linux-arm-musl': 2.5.6 + '@parcel/watcher-linux-arm64-glibc': 2.5.6 + '@parcel/watcher-linux-arm64-musl': 2.5.6 + '@parcel/watcher-linux-x64-glibc': 2.5.6 + '@parcel/watcher-linux-x64-musl': 2.5.6 + '@parcel/watcher-win32-arm64': 2.5.6 + '@parcel/watcher-win32-ia32': 2.5.6 + '@parcel/watcher-win32-x64': 2.5.6 + optional: true + + '@peculiar/asn1-schema@2.6.0': + dependencies: + asn1js: 3.0.7 + pvtsutils: 1.3.6 + tslib: 2.8.1 + + '@peculiar/json-schema@1.1.12': + dependencies: + tslib: 2.8.1 + + '@peculiar/webcrypto@1.5.0': + dependencies: + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/json-schema': 1.1.12 + pvtsutils: 1.3.6 + tslib: 2.8.1 + webcrypto-core: 1.8.1 + + '@phosphor-icons/react@2.1.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@playwright/test@1.58.2': + dependencies: + playwright: 1.58.2 + + '@popperjs/core@2.11.8': {} + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rollup/pluginutils@5.3.0(rollup@4.59.0)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.3 + optionalDependencies: + rollup: 4.59.0 + + '@rollup/rollup-android-arm-eabi@4.59.0': + optional: true + + '@rollup/rollup-android-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-x64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.59.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.59.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.59.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.59.0': + optional: true + + '@rtsao/scc@1.1.0': {} + + '@rushstack/node-core-library@5.20.3(@types/node@24.12.0)': + dependencies: + ajv: 8.18.0 + ajv-draft-04: 1.0.0(ajv@8.18.0) + ajv-formats: 3.0.1(ajv@8.18.0) + fs-extra: 11.3.4 + import-lazy: 4.0.0 + jju: 1.4.0 + resolve: 1.22.11 + semver: 7.5.4 + optionalDependencies: + '@types/node': 24.12.0 + + '@rushstack/problem-matcher@0.2.1(@types/node@24.12.0)': + optionalDependencies: + '@types/node': 24.12.0 + + '@rushstack/rig-package@0.7.2': + dependencies: + resolve: 1.22.11 + strip-json-comments: 3.1.1 + + '@rushstack/terminal@0.22.3(@types/node@24.12.0)': + dependencies: + '@rushstack/node-core-library': 5.20.3(@types/node@24.12.0) + '@rushstack/problem-matcher': 0.2.1(@types/node@24.12.0) + supports-color: 8.1.1 + optionalDependencies: + '@types/node': 24.12.0 + + '@rushstack/ts-command-line@5.3.3(@types/node@24.12.0)': + dependencies: + '@rushstack/terminal': 0.22.3(@types/node@24.12.0) + '@types/argparse': 1.0.38 + argparse: 1.0.10 + string-argv: 0.3.2 + transitivePeerDependencies: + - '@types/node' + + '@scarf/scarf@1.4.0': {} + + '@sindresorhus/to-milliseconds@1.2.0': {} + + '@standard-schema/utils@0.3.0': {} + + '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + + '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + + '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + + '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + + '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + + '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + + '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + + '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + + '@svgr/babel-preset@8.1.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.29.0) + '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.29.0) + '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.29.0) + '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.29.0) + '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.29.0) + '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.29.0) + '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.29.0) + '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.29.0) + + '@svgr/core@8.1.0(typescript@5.9.3)': + dependencies: + '@babel/core': 7.29.0 + '@svgr/babel-preset': 8.1.0(@babel/core@7.29.0) + camelcase: 6.3.0 + cosmiconfig: 8.3.6(typescript@5.9.3) + snake-case: 3.0.4 + transitivePeerDependencies: + - supports-color + - typescript + + '@svgr/hast-util-to-babel-ast@8.0.0': + dependencies: + '@babel/types': 7.29.0 + entities: 4.5.0 + + '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.9.3))': + dependencies: + '@babel/core': 7.29.0 + '@svgr/babel-preset': 8.1.0(@babel/core@7.29.0) + '@svgr/core': 8.1.0(typescript@5.9.3) + '@svgr/hast-util-to-babel-ast': 8.0.0 + svg-parser: 2.0.4 + transitivePeerDependencies: + - supports-color + + '@swagger-api/apidom-ast@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@swagger-api/apidom-error': 1.6.0 + '@types/ramda': 0.30.2 + ramda: 0.30.1 + ramda-adjunct: 5.1.0(ramda@0.30.1) + unraw: 3.0.0 + + '@swagger-api/apidom-core@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@swagger-api/apidom-ast': 1.6.0 + '@swagger-api/apidom-error': 1.6.0 + '@types/ramda': 0.30.2 + minim: 0.23.8 + ramda: 0.30.1 + ramda-adjunct: 5.1.0(ramda@0.30.1) + short-unique-id: 5.3.2 + ts-mixer: 6.0.4 + + '@swagger-api/apidom-error@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + + '@swagger-api/apidom-json-pointer@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@swagger-api/apidom-core': 1.6.0 + '@swagger-api/apidom-error': 1.6.0 + '@swaggerexpert/json-pointer': 2.10.2 + + '@swagger-api/apidom-ns-api-design-systems@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@swagger-api/apidom-core': 1.6.0 + '@swagger-api/apidom-error': 1.6.0 + '@swagger-api/apidom-ns-openapi-3-1': 1.6.0 + '@types/ramda': 0.30.2 + ramda: 0.30.1 + ramda-adjunct: 5.1.0(ramda@0.30.1) + ts-mixer: 6.0.4 + optional: true + + '@swagger-api/apidom-ns-arazzo-1@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@swagger-api/apidom-core': 1.6.0 + '@swagger-api/apidom-ns-json-schema-2020-12': 1.6.0 + '@types/ramda': 0.30.2 + ramda: 0.30.1 + ramda-adjunct: 5.1.0(ramda@0.30.1) + ts-mixer: 6.0.4 + optional: true + + '@swagger-api/apidom-ns-asyncapi-2@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@swagger-api/apidom-core': 1.6.0 + '@swagger-api/apidom-ns-json-schema-draft-7': 1.6.0 + '@types/ramda': 0.30.2 + ramda: 0.30.1 + ramda-adjunct: 5.1.0(ramda@0.30.1) + ts-mixer: 6.0.4 + optional: true + + '@swagger-api/apidom-ns-asyncapi-3@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@swagger-api/apidom-core': 1.6.0 + '@swagger-api/apidom-ns-asyncapi-2': 1.6.0 + '@types/ramda': 0.30.2 + ramda: 0.30.1 + ramda-adjunct: 5.1.0(ramda@0.30.1) + ts-mixer: 6.0.4 + optional: true + + '@swagger-api/apidom-ns-json-schema-2019-09@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@swagger-api/apidom-core': 1.6.0 + '@swagger-api/apidom-error': 1.6.0 + '@swagger-api/apidom-ns-json-schema-draft-7': 1.6.0 + '@types/ramda': 0.30.2 + ramda: 0.30.1 + ramda-adjunct: 5.1.0(ramda@0.30.1) + ts-mixer: 6.0.4 + + '@swagger-api/apidom-ns-json-schema-2020-12@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@swagger-api/apidom-core': 1.6.0 + '@swagger-api/apidom-error': 1.6.0 + '@swagger-api/apidom-ns-json-schema-2019-09': 1.6.0 + '@types/ramda': 0.30.2 + ramda: 0.30.1 + ramda-adjunct: 5.1.0(ramda@0.30.1) + ts-mixer: 6.0.4 + + '@swagger-api/apidom-ns-json-schema-draft-4@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@swagger-api/apidom-ast': 1.6.0 + '@swagger-api/apidom-core': 1.6.0 + '@types/ramda': 0.30.2 + ramda: 0.30.1 + ramda-adjunct: 5.1.0(ramda@0.30.1) + ts-mixer: 6.0.4 + + '@swagger-api/apidom-ns-json-schema-draft-6@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@swagger-api/apidom-core': 1.6.0 + '@swagger-api/apidom-error': 1.6.0 + '@swagger-api/apidom-ns-json-schema-draft-4': 1.6.0 + '@types/ramda': 0.30.2 + ramda: 0.30.1 + ramda-adjunct: 5.1.0(ramda@0.30.1) + ts-mixer: 6.0.4 + + '@swagger-api/apidom-ns-json-schema-draft-7@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@swagger-api/apidom-core': 1.6.0 + '@swagger-api/apidom-error': 1.6.0 + '@swagger-api/apidom-ns-json-schema-draft-6': 1.6.0 + '@types/ramda': 0.30.2 + ramda: 0.30.1 + ramda-adjunct: 5.1.0(ramda@0.30.1) + ts-mixer: 6.0.4 + + '@swagger-api/apidom-ns-openapi-2@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@swagger-api/apidom-core': 1.6.0 + '@swagger-api/apidom-error': 1.6.0 + '@swagger-api/apidom-ns-json-schema-draft-4': 1.6.0 + '@types/ramda': 0.30.2 + ramda: 0.30.1 + ramda-adjunct: 5.1.0(ramda@0.30.1) + ts-mixer: 6.0.4 + optional: true + + '@swagger-api/apidom-ns-openapi-3-0@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@swagger-api/apidom-core': 1.6.0 + '@swagger-api/apidom-error': 1.6.0 + '@swagger-api/apidom-ns-json-schema-draft-4': 1.6.0 + '@types/ramda': 0.30.2 + ramda: 0.30.1 + ramda-adjunct: 5.1.0(ramda@0.30.1) + ts-mixer: 6.0.4 + + '@swagger-api/apidom-ns-openapi-3-1@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@swagger-api/apidom-ast': 1.6.0 + '@swagger-api/apidom-core': 1.6.0 + '@swagger-api/apidom-json-pointer': 1.6.0 + '@swagger-api/apidom-ns-json-schema-2020-12': 1.6.0 + '@swagger-api/apidom-ns-openapi-3-0': 1.6.0 + '@types/ramda': 0.30.2 + ramda: 0.30.1 + ramda-adjunct: 5.1.0(ramda@0.30.1) + ts-mixer: 6.0.4 + + '@swagger-api/apidom-ns-openapi-3-2@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@swagger-api/apidom-ast': 1.6.0 + '@swagger-api/apidom-core': 1.6.0 + '@swagger-api/apidom-json-pointer': 1.6.0 + '@swagger-api/apidom-ns-json-schema-2020-12': 1.6.0 + '@swagger-api/apidom-ns-openapi-3-0': 1.6.0 + '@swagger-api/apidom-ns-openapi-3-1': 1.6.0 + '@types/ramda': 0.30.2 + ramda: 0.30.1 + ramda-adjunct: 5.1.0(ramda@0.30.1) + ts-mixer: 6.0.4 + + '@swagger-api/apidom-parser-adapter-api-design-systems-json@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@swagger-api/apidom-core': 1.6.0 + '@swagger-api/apidom-ns-api-design-systems': 1.6.0 + '@swagger-api/apidom-parser-adapter-json': 1.6.0 + '@types/ramda': 0.30.2 + ramda: 0.30.1 + ramda-adjunct: 5.1.0(ramda@0.30.1) + optional: true + + '@swagger-api/apidom-parser-adapter-api-design-systems-yaml@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@swagger-api/apidom-core': 1.6.0 + '@swagger-api/apidom-ns-api-design-systems': 1.6.0 + '@swagger-api/apidom-parser-adapter-yaml-1-2': 1.6.0 + '@types/ramda': 0.30.2 + ramda: 0.30.1 + ramda-adjunct: 5.1.0(ramda@0.30.1) + optional: true + + '@swagger-api/apidom-parser-adapter-arazzo-json-1@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@swagger-api/apidom-core': 1.6.0 + '@swagger-api/apidom-ns-arazzo-1': 1.6.0 + '@swagger-api/apidom-parser-adapter-json': 1.6.0 + '@types/ramda': 0.30.2 + ramda: 0.30.1 + ramda-adjunct: 5.1.0(ramda@0.30.1) + optional: true + + '@swagger-api/apidom-parser-adapter-arazzo-yaml-1@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@swagger-api/apidom-core': 1.6.0 + '@swagger-api/apidom-ns-arazzo-1': 1.6.0 + '@swagger-api/apidom-parser-adapter-yaml-1-2': 1.6.0 + '@types/ramda': 0.30.2 + ramda: 0.30.1 + ramda-adjunct: 5.1.0(ramda@0.30.1) + optional: true + + '@swagger-api/apidom-parser-adapter-asyncapi-json-2@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@swagger-api/apidom-core': 1.6.0 + '@swagger-api/apidom-ns-asyncapi-2': 1.6.0 + '@swagger-api/apidom-parser-adapter-json': 1.6.0 + '@types/ramda': 0.30.2 + ramda: 0.30.1 + ramda-adjunct: 5.1.0(ramda@0.30.1) + optional: true + + '@swagger-api/apidom-parser-adapter-asyncapi-json-3@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@swagger-api/apidom-core': 1.6.0 + '@swagger-api/apidom-ns-asyncapi-3': 1.6.0 + '@swagger-api/apidom-parser-adapter-json': 1.6.0 + '@types/ramda': 0.30.2 + ramda: 0.30.1 + ramda-adjunct: 5.1.0(ramda@0.30.1) + optional: true + + '@swagger-api/apidom-parser-adapter-asyncapi-yaml-2@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@swagger-api/apidom-core': 1.6.0 + '@swagger-api/apidom-ns-asyncapi-2': 1.6.0 + '@swagger-api/apidom-parser-adapter-yaml-1-2': 1.6.0 + '@types/ramda': 0.30.2 + ramda: 0.30.1 + ramda-adjunct: 5.1.0(ramda@0.30.1) + optional: true + + '@swagger-api/apidom-parser-adapter-asyncapi-yaml-3@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@swagger-api/apidom-core': 1.6.0 + '@swagger-api/apidom-ns-asyncapi-3': 1.6.0 + '@swagger-api/apidom-parser-adapter-yaml-1-2': 1.6.0 + '@types/ramda': 0.30.2 + ramda: 0.30.1 + ramda-adjunct: 5.1.0(ramda@0.30.1) + optional: true + + '@swagger-api/apidom-parser-adapter-json@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@swagger-api/apidom-ast': 1.6.0 + '@swagger-api/apidom-core': 1.6.0 + '@swagger-api/apidom-error': 1.6.0 + '@types/ramda': 0.30.2 + ramda: 0.30.1 + ramda-adjunct: 5.1.0(ramda@0.30.1) + tree-sitter: 0.21.1 + tree-sitter-json: 0.24.8(tree-sitter@0.21.1) + web-tree-sitter: 0.24.5 + optional: true + + '@swagger-api/apidom-parser-adapter-openapi-json-2@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@swagger-api/apidom-core': 1.6.0 + '@swagger-api/apidom-ns-openapi-2': 1.6.0 + '@swagger-api/apidom-parser-adapter-json': 1.6.0 + '@types/ramda': 0.30.2 + ramda: 0.30.1 + ramda-adjunct: 5.1.0(ramda@0.30.1) + optional: true + + '@swagger-api/apidom-parser-adapter-openapi-json-3-0@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@swagger-api/apidom-core': 1.6.0 + '@swagger-api/apidom-ns-openapi-3-0': 1.6.0 + '@swagger-api/apidom-parser-adapter-json': 1.6.0 + '@types/ramda': 0.30.2 + ramda: 0.30.1 + ramda-adjunct: 5.1.0(ramda@0.30.1) + optional: true + + '@swagger-api/apidom-parser-adapter-openapi-json-3-1@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@swagger-api/apidom-core': 1.6.0 + '@swagger-api/apidom-ns-openapi-3-1': 1.6.0 + '@swagger-api/apidom-parser-adapter-json': 1.6.0 + '@types/ramda': 0.30.2 + ramda: 0.30.1 + ramda-adjunct: 5.1.0(ramda@0.30.1) + optional: true + + '@swagger-api/apidom-parser-adapter-openapi-json-3-2@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@swagger-api/apidom-core': 1.6.0 + '@swagger-api/apidom-ns-openapi-3-2': 1.6.0 + '@swagger-api/apidom-parser-adapter-json': 1.6.0 + '@types/ramda': 0.30.2 + ramda: 0.30.1 + ramda-adjunct: 5.1.0(ramda@0.30.1) + optional: true + + '@swagger-api/apidom-parser-adapter-openapi-yaml-2@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@swagger-api/apidom-core': 1.6.0 + '@swagger-api/apidom-ns-openapi-2': 1.6.0 + '@swagger-api/apidom-parser-adapter-yaml-1-2': 1.6.0 + '@types/ramda': 0.30.2 + ramda: 0.30.1 + ramda-adjunct: 5.1.0(ramda@0.30.1) + optional: true + + '@swagger-api/apidom-parser-adapter-openapi-yaml-3-0@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@swagger-api/apidom-core': 1.6.0 + '@swagger-api/apidom-ns-openapi-3-0': 1.6.0 + '@swagger-api/apidom-parser-adapter-yaml-1-2': 1.6.0 + '@types/ramda': 0.30.2 + ramda: 0.30.1 + ramda-adjunct: 5.1.0(ramda@0.30.1) + optional: true + + '@swagger-api/apidom-parser-adapter-openapi-yaml-3-1@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@swagger-api/apidom-core': 1.6.0 + '@swagger-api/apidom-ns-openapi-3-1': 1.6.0 + '@swagger-api/apidom-parser-adapter-yaml-1-2': 1.6.0 + '@types/ramda': 0.30.2 + ramda: 0.30.1 + ramda-adjunct: 5.1.0(ramda@0.30.1) + optional: true + + '@swagger-api/apidom-parser-adapter-openapi-yaml-3-2@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@swagger-api/apidom-core': 1.6.0 + '@swagger-api/apidom-ns-openapi-3-2': 1.6.0 + '@swagger-api/apidom-parser-adapter-yaml-1-2': 1.6.0 + '@types/ramda': 0.30.2 + ramda: 0.30.1 + ramda-adjunct: 5.1.0(ramda@0.30.1) + optional: true + + '@swagger-api/apidom-parser-adapter-yaml-1-2@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@swagger-api/apidom-ast': 1.6.0 + '@swagger-api/apidom-core': 1.6.0 + '@swagger-api/apidom-error': 1.6.0 + '@tree-sitter-grammars/tree-sitter-yaml': 0.7.1(tree-sitter@0.22.4) + '@types/ramda': 0.30.2 + ramda: 0.30.1 + ramda-adjunct: 5.1.0(ramda@0.30.1) + tree-sitter: 0.22.4 + web-tree-sitter: 0.24.5 + optional: true + + '@swagger-api/apidom-reference@1.6.0': + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@swagger-api/apidom-core': 1.6.0 + '@swagger-api/apidom-error': 1.6.0 + '@types/ramda': 0.30.2 + axios: 1.13.6 + minimatch: 10.2.4 + ramda: 0.30.1 + ramda-adjunct: 5.1.0(ramda@0.30.1) + optionalDependencies: + '@swagger-api/apidom-json-pointer': 1.6.0 + '@swagger-api/apidom-ns-arazzo-1': 1.6.0 + '@swagger-api/apidom-ns-asyncapi-2': 1.6.0 + '@swagger-api/apidom-ns-openapi-2': 1.6.0 + '@swagger-api/apidom-ns-openapi-3-0': 1.6.0 + '@swagger-api/apidom-ns-openapi-3-1': 1.6.0 + '@swagger-api/apidom-ns-openapi-3-2': 1.6.0 + '@swagger-api/apidom-parser-adapter-api-design-systems-json': 1.6.0 + '@swagger-api/apidom-parser-adapter-api-design-systems-yaml': 1.6.0 + '@swagger-api/apidom-parser-adapter-arazzo-json-1': 1.6.0 + '@swagger-api/apidom-parser-adapter-arazzo-yaml-1': 1.6.0 + '@swagger-api/apidom-parser-adapter-asyncapi-json-2': 1.6.0 + '@swagger-api/apidom-parser-adapter-asyncapi-json-3': 1.6.0 + '@swagger-api/apidom-parser-adapter-asyncapi-yaml-2': 1.6.0 + '@swagger-api/apidom-parser-adapter-asyncapi-yaml-3': 1.6.0 + '@swagger-api/apidom-parser-adapter-json': 1.6.0 + '@swagger-api/apidom-parser-adapter-openapi-json-2': 1.6.0 + '@swagger-api/apidom-parser-adapter-openapi-json-3-0': 1.6.0 + '@swagger-api/apidom-parser-adapter-openapi-json-3-1': 1.6.0 + '@swagger-api/apidom-parser-adapter-openapi-json-3-2': 1.6.0 + '@swagger-api/apidom-parser-adapter-openapi-yaml-2': 1.6.0 + '@swagger-api/apidom-parser-adapter-openapi-yaml-3-0': 1.6.0 + '@swagger-api/apidom-parser-adapter-openapi-yaml-3-1': 1.6.0 + '@swagger-api/apidom-parser-adapter-openapi-yaml-3-2': 1.6.0 + '@swagger-api/apidom-parser-adapter-yaml-1-2': 1.6.0 + transitivePeerDependencies: + - debug + + '@swaggerexpert/cookie@2.0.2': + dependencies: + apg-lite: 1.0.5 + + '@swaggerexpert/json-pointer@2.10.2': + dependencies: + apg-lite: 1.0.5 + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/runtime': 7.28.6 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.9.1': + dependencies: + '@adobe/css-tools': 4.4.4 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.6 + '@testing-library/dom': 10.4.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': + dependencies: + '@testing-library/dom': 10.4.1 + + '@tree-sitter-grammars/tree-sitter-yaml@0.7.1(tree-sitter@0.22.4)': + dependencies: + node-addon-api: 8.6.0 + node-gyp-build: 4.8.4 + optionalDependencies: + tree-sitter: 0.22.4 + optional: true + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/argparse@1.0.38': {} + + '@types/aria-query@5.0.4': {} + + '@types/autosuggest-highlight@3.2.3': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/backbone@1.4.23': + dependencies: + '@types/jquery': 3.5.34 + '@types/underscore': 1.13.0 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/d3-array@3.2.2': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + + '@types/deep-eql@4.0.2': {} + + '@types/dom-to-image@2.6.7': {} + + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.8 + + '@types/estree@1.0.8': {} + + '@types/hammerjs@2.0.46': {} + + '@types/hast@2.3.10': + dependencies: + '@types/unist': 2.0.11 + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/jquery@3.5.34': + dependencies: + '@types/sizzle': 2.3.10 + + '@types/jqueryui@1.12.24': + dependencies: + '@types/jquery': 3.5.34 + + '@types/json-schema@7.0.15': {} + + '@types/json5@0.0.29': {} + + '@types/lodash@4.17.24': {} + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@2.1.0': {} + + '@types/node@24.12.0': + dependencies: + undici-types: 7.16.0 + + '@types/parse-json@4.0.2': {} + + '@types/prismjs@1.26.6': {} + + '@types/prop-types@15.7.15': {} + + '@types/q@1.5.8': {} + + '@types/qs@6.15.0': {} + + '@types/ramda@0.30.2': + dependencies: + types-ramda: 0.30.1 + + '@types/react-beautiful-dnd@13.1.8': + dependencies: + '@types/react': 18.3.28 + + '@types/react-datepicker@6.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react': 0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/react': 18.3.28 + date-fns: 3.6.0 + transitivePeerDependencies: + - react + - react-dom + + '@types/react-dom@18.3.7(@types/react@18.3.28)': + dependencies: + '@types/react': 18.3.28 + + '@types/react-helmet@6.1.11': + dependencies: + '@types/react': 18.3.28 + + '@types/react-highlight@0.12.8': + dependencies: + '@types/react': 18.3.28 + + '@types/react-syntax-highlighter@15.5.13': + dependencies: + '@types/react': 18.3.28 + + '@types/react-transition-group@4.4.12(@types/react@18.3.28)': + dependencies: + '@types/react': 18.3.28 + + '@types/react@18.3.28': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + + '@types/selectize@0.12.39': {} + + '@types/sizzle@2.3.10': {} + + '@types/swagger-ui-react@5.18.0': + dependencies: + '@types/react': 18.3.28 + + '@types/trusted-types@2.0.7': + optional: true + + '@types/underscore@1.13.0': {} + + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + + '@types/url-parse@1.4.11': {} + + '@types/use-sync-external-store@0.0.6': {} + + '@types/uuid@8.3.4': {} + + '@types/yup@0.32.0': + dependencies: + yup: 1.7.1 + + '@typescript-eslint/eslint-plugin@8.57.0(@typescript-eslint/parser@8.57.0(eslint@9.32.0)(typescript@5.9.3))(eslint@9.32.0)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.57.0(eslint@9.32.0)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.57.0 + '@typescript-eslint/type-utils': 8.57.0(eslint@9.32.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.0(eslint@9.32.0)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.57.0 + eslint: 9.32.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.57.0(eslint@9.32.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.57.0 + '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/typescript-estree': 8.57.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.57.0 + debug: 4.4.3(supports-color@5.5.0) + eslint: 9.32.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.57.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.57.0(typescript@5.9.3) + '@typescript-eslint/types': 8.57.0 + debug: 4.4.3(supports-color@5.5.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.57.0': + dependencies: + '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/visitor-keys': 8.57.0 + + '@typescript-eslint/tsconfig-utils@8.57.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.57.0(eslint@9.32.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/typescript-estree': 8.57.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.0(eslint@9.32.0)(typescript@5.9.3) + debug: 4.4.3(supports-color@5.5.0) + eslint: 9.32.0 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.57.0': {} + + '@typescript-eslint/typescript-estree@8.57.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.57.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.57.0(typescript@5.9.3) + '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/visitor-keys': 8.57.0 + debug: 4.4.3(supports-color@5.5.0) + minimatch: 10.2.4 + semver: 7.7.4 + tinyglobby: 0.2.15 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.57.0(eslint@9.32.0)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.32.0) + '@typescript-eslint/scope-manager': 8.57.0 + '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/typescript-estree': 8.57.0(typescript@5.9.3) + eslint: 9.32.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.57.0': + dependencies: + '@typescript-eslint/types': 8.57.0 + eslint-visitor-keys: 5.0.1 + + '@ungap/structured-clone@1.3.0': {} + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + optional: true + + '@unrs/resolver-binding-android-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + dependencies: + '@napi-rs/wasm-runtime': 0.2.12 + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + optional: true + + '@use-gesture/core@10.3.1': {} + + '@use-gesture/react@10.3.1(react@18.3.1)': + dependencies: + '@use-gesture/core': 10.3.1 + react: 18.3.1 + + '@vitejs/plugin-react@4.7.0(vite@7.3.1(@types/node@24.12.0)(sass@1.97.3))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 7.3.1(@types/node@24.12.0)(sass@1.97.3) + transitivePeerDependencies: + - supports-color + + '@vitest/eslint-plugin@1.6.10(eslint@9.32.0)(typescript@5.9.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jsdom@26.1.0)(sass@1.97.3))': + dependencies: + '@typescript-eslint/scope-manager': 8.57.0 + '@typescript-eslint/utils': 8.57.0(eslint@9.32.0)(typescript@5.9.3) + eslint: 9.32.0 + optionalDependencies: + typescript: 5.9.3 + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jsdom@26.1.0)(sass@1.97.3) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@24.12.0)(sass@1.97.3))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.1(@types/node@24.12.0)(sass@1.97.3) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + '@volar/language-core@2.4.28': + dependencies: + '@volar/source-map': 2.4.28 + + '@volar/source-map@2.4.28': {} + + '@volar/typescript@2.4.28': + dependencies: + '@volar/language-core': 2.4.28 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@vue/compiler-core@3.5.30': + dependencies: + '@babel/parser': 7.29.0 + '@vue/shared': 3.5.30 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.30': + dependencies: + '@vue/compiler-core': 3.5.30 + '@vue/shared': 3.5.30 + + '@vue/compiler-vue2@2.7.16': + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + + '@vue/language-core@2.2.0(typescript@5.9.3)': + dependencies: + '@volar/language-core': 2.4.28 + '@vue/compiler-dom': 3.5.30 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.5.30 + alien-signals: 0.4.14 + minimatch: 9.0.9 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + optionalDependencies: + typescript: 5.9.3 + + '@vue/shared@3.5.30': {} + + '@xstate/inspect@0.8.0(ws@8.19.0)(xstate@4.38.3)': + dependencies: + fast-safe-stringify: 2.1.1 + ws: 8.19.0 + xstate: 4.38.3 + + '@xstate/react@3.2.2(@types/react@18.3.28)(react@18.3.1)(xstate@4.38.3)': + dependencies: + react: 18.3.1 + use-isomorphic-layout-effect: 1.2.1(@types/react@18.3.28)(react@18.3.1) + use-sync-external-store: 1.6.0(react@18.3.1) + optionalDependencies: + xstate: 4.38.3 + transitivePeerDependencies: + - '@types/react' + + Base64@1.1.0: {} + + abortcontroller-polyfill@1.7.8: {} + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + agent-base@7.1.4: {} + + ajv-draft-04@1.0.0(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv-errors@3.0.0(ajv@8.18.0): + dependencies: + ajv: 8.18.0 + + ajv-formats@2.1.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv@6.14.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + alien-signals@0.4.14: {} + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + apg-lite@1.0.5: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + asn1js@3.0.7: + dependencies: + pvtsutils: 1.3.6 + pvutils: 1.1.5 + tslib: 2.8.1 + + assertion-error@2.0.1: {} + + async-function@1.0.0: {} + + asynckit@0.4.0: {} + + atob@2.1.2: {} + + autolinker@3.16.2: + dependencies: + tslib: 2.8.1 + + autosuggest-highlight@3.3.4: + dependencies: + remove-accents: 0.4.4 + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + axios@1.13.6: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + babel-plugin-macros@3.1.0: + dependencies: + '@babel/runtime': 7.28.6 + cosmiconfig: 7.1.0 + resolve: 1.22.11 + + babel-plugin-styled-components@2.1.4(@babel/core@7.29.0)(styled-components@5.3.11(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react-is@19.2.4)(react@18.3.1))(supports-color@5.5.0): + dependencies: + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-module-imports': 7.28.6(supports-color@5.5.0) + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + lodash: 4.17.23 + picomatch: 2.3.1 + styled-components: 5.3.11(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react-is@19.2.4)(react@18.3.1) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + bail@2.0.2: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + base64-js@1.5.1: {} + + baseline-browser-mapping@2.10.0: {} + + batch-processor@1.0.0: {} + + big-integer@1.6.52: {} + + birecord@0.1.1: {} + + body-scroll-lock-upgrade@1.1.0: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.4: + dependencies: + balanced-match: 4.0.4 + + broadcast-channel@3.7.0: + dependencies: + '@babel/runtime': 7.28.6 + detect-node: 2.1.0 + js-sha3: 0.8.0 + microseconds: 0.2.0 + nano-time: 1.0.0 + oblivious-set: 1.0.0 + rimraf: 3.0.2 + unload: 2.2.0 + + broadcast-channel@4.17.0: + dependencies: + '@babel/runtime': 7.28.6 + oblivious-set: 1.1.1 + p-queue: 6.6.2 + rimraf: 3.0.2 + unload: 2.3.1 + + browser-tabs-lock@1.3.0: + dependencies: + lodash: 4.17.23 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.10.0 + caniuse-lite: 1.0.30001777 + electron-to-chromium: 1.5.307 + node-releases: 2.0.36 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + btoa@1.2.1: {} + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + cac@6.7.14: {} + + calculate-size@1.1.1: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + camelcase@6.3.0: {} + + camelize@1.0.1: {} + + caniuse-lite@1.0.30001777: {} + + ccount@2.0.1: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + character-entities-html4@2.1.0: {} + + character-entities-legacy@1.1.4: {} + + character-entities-legacy@3.0.0: {} + + character-entities@1.2.4: {} + + character-entities@2.0.2: {} + + character-reference-invalid@1.1.4: {} + + character-reference-invalid@2.0.1: {} + + check-error@2.1.3: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + classnames@2.5.1: {} + + client-only@0.0.1: {} + + clipboard@1.7.1: + dependencies: + good-listener: 1.2.2 + select: 1.1.2 + tiny-emitter: 2.1.0 + + clone@2.1.2: {} + + clsx@2.1.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.4 + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + comma-separated-tokens@1.0.8: {} + + comma-separated-tokens@2.0.3: {} + + compare-versions@4.1.4: {} + + compare-versions@6.1.1: {} + + component-emitter@1.3.1: {} + + concat-map@0.0.1: {} + + confbox@0.1.8: {} + + confbox@0.2.4: {} + + container-query-toolkit@0.1.3: {} + + convert-source-map@1.9.0: {} + + convert-source-map@2.0.0: {} + + cookie@1.1.1: {} + + copy-to-clipboard@3.3.3: + dependencies: + toggle-selection: 1.0.6 + + core-js-pure@3.48.0: {} + + core-js@3.48.0: {} + + cosmiconfig@7.1.0: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.1 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 + + cosmiconfig@8.3.6(typescript@5.9.3): + dependencies: + import-fresh: 3.3.1 + js-yaml: 4.1.1 + parse-json: 5.2.0 + path-type: 4.0.0 + optionalDependencies: + typescript: 5.9.3 + + cron-validate@1.5.3: + dependencies: + yup: 1.7.1 + + cronstrue@2.61.0: {} + + cross-fetch@3.2.0: + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-color-keywords@1.0.0: {} + + css-to-react-native@3.2.0: + dependencies: + camelize: 1.0.1 + css-color-keywords: 1.0.0 + postcss-value-parser: 4.2.0 + + css.escape@1.5.1: {} + + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + + csstype@3.2.3: {} + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-ease@3.0.1: {} + + d3-format@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + date-fns-tz@2.0.1(date-fns@2.30.0): + dependencies: + date-fns: 2.30.0 + + date-fns@2.30.0: + dependencies: + '@babel/runtime': 7.28.6 + + date-fns@3.6.0: {} + + dayjs@1.10.7: {} + + de-indent@1.0.2: {} + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.4.3(supports-color@5.5.0): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 5.5.0 + + decimal.js-light@2.5.1: {} + + decimal.js@10.6.0: {} + + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + + deep-copy@1.4.2: {} + + deep-eql@5.0.2: {} + + deep-equal@1.1.2: + dependencies: + is-arguments: 1.2.0 + is-date-object: 1.1.0 + is-regex: 1.1.4 + object-is: 1.1.6 + object-keys: 1.1.1 + regexp.prototype.flags: 1.5.4 + + deep-extend@0.6.0: {} + + deep-is@0.1.4: {} + + deepmerge@4.3.1: {} + + defaulty@2.1.0: + dependencies: + deep-copy: 1.4.2 + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + defined@1.0.1: {} + + delayed-stream@1.0.0: {} + + delegate@3.2.0: {} + + dequal@2.0.3: {} + + detect-libc@2.1.2: + optional: true + + detect-node@2.1.0: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + diff@8.0.3: {} + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.28.6 + csstype: 3.2.3 + + dom-mutator@0.6.0: {} + + dom-to-image@2.6.0: {} + + dompurify@3.2.6: + optionalDependencies: + '@types/trusted-types': 2.0.7 + + dompurify@3.2.7: + optionalDependencies: + '@types/trusted-types': 2.0.7 + + dot-case@3.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + + dotenv@16.6.1: {} + + dotignore@0.1.2: + dependencies: + minimatch: 3.1.5 + + drange@1.1.1: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + electron-to-chromium@1.5.307: {} + + element-resize-detector@1.1.13: + dependencies: + batch-processor: 1.0.0 + + elkjs@0.8.2: {} + + ellipsize@0.2.0: + dependencies: + tape: 4.17.0 + + entities@4.5.0: {} + + entities@6.0.1: {} + + entities@7.0.1: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-abstract@1.24.1: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.20 + + es-cookie@1.3.2: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@1.7.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + escape-string-regexp@5.0.0: {} + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.1 + resolve: 1.22.11 + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.3(supports-color@5.5.0) + eslint: 9.32.0 + get-tsconfig: 4.13.6 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.15 + unrs-resolver: 1.11.1 + optionalDependencies: + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.0(eslint@9.32.0)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.32.0) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.57.0(eslint@9.32.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.32.0): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.57.0(eslint@9.32.0)(typescript@5.9.3) + eslint: 9.32.0 + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0) + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.0(eslint@9.32.0)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.32.0): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.32.0 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.57.0(eslint@9.32.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.32.0) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.5 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.57.0(eslint@9.32.0)(typescript@5.9.3) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-react-debug@1.53.1(eslint@9.32.0)(typescript@5.9.3): + dependencies: + '@eslint-react/ast': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@eslint-react/core': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@eslint-react/eff': 1.53.1 + '@eslint-react/kit': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@eslint-react/shared': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@eslint-react/var': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.57.0 + '@typescript-eslint/type-utils': 8.57.0(eslint@9.32.0)(typescript@5.9.3) + '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/utils': 8.57.0(eslint@9.32.0)(typescript@5.9.3) + eslint: 9.32.0 + string-ts: 2.3.1 + ts-pattern: 5.9.0 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-dom@1.53.1(eslint@9.32.0)(typescript@5.9.3): + dependencies: + '@eslint-react/ast': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@eslint-react/core': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@eslint-react/eff': 1.53.1 + '@eslint-react/kit': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@eslint-react/shared': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@eslint-react/var': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.57.0 + '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/utils': 8.57.0(eslint@9.32.0)(typescript@5.9.3) + compare-versions: 6.1.1 + eslint: 9.32.0 + string-ts: 2.3.1 + ts-pattern: 5.9.0 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-hooks-extra@1.53.1(eslint@9.32.0)(typescript@5.9.3): + dependencies: + '@eslint-react/ast': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@eslint-react/core': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@eslint-react/eff': 1.53.1 + '@eslint-react/kit': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@eslint-react/shared': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@eslint-react/var': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.57.0 + '@typescript-eslint/type-utils': 8.57.0(eslint@9.32.0)(typescript@5.9.3) + '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/utils': 8.57.0(eslint@9.32.0)(typescript@5.9.3) + eslint: 9.32.0 + string-ts: 2.3.1 + ts-pattern: 5.9.0 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-hooks@5.2.0(eslint@9.32.0): + dependencies: + eslint: 9.32.0 + + eslint-plugin-react-naming-convention@1.53.1(eslint@9.32.0)(typescript@5.9.3): + dependencies: + '@eslint-react/ast': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@eslint-react/core': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@eslint-react/eff': 1.53.1 + '@eslint-react/kit': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@eslint-react/shared': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@eslint-react/var': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.57.0 + '@typescript-eslint/type-utils': 8.57.0(eslint@9.32.0)(typescript@5.9.3) + '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/utils': 8.57.0(eslint@9.32.0)(typescript@5.9.3) + eslint: 9.32.0 + string-ts: 2.3.1 + ts-pattern: 5.9.0 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-refresh@0.4.26(eslint@9.32.0): + dependencies: + eslint: 9.32.0 + + eslint-plugin-react-web-api@1.53.1(eslint@9.32.0)(typescript@5.9.3): + dependencies: + '@eslint-react/ast': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@eslint-react/core': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@eslint-react/eff': 1.53.1 + '@eslint-react/kit': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@eslint-react/shared': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@eslint-react/var': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.57.0 + '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/utils': 8.57.0(eslint@9.32.0)(typescript@5.9.3) + eslint: 9.32.0 + string-ts: 2.3.1 + ts-pattern: 5.9.0 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-x@1.53.1(eslint@9.32.0)(ts-api-utils@2.4.0(typescript@5.9.3))(typescript@5.9.3): + dependencies: + '@eslint-react/ast': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@eslint-react/core': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@eslint-react/eff': 1.53.1 + '@eslint-react/kit': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@eslint-react/shared': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@eslint-react/var': 1.53.1(eslint@9.32.0)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.57.0 + '@typescript-eslint/type-utils': 8.57.0(eslint@9.32.0)(typescript@5.9.3) + '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/utils': 8.57.0(eslint@9.32.0)(typescript@5.9.3) + compare-versions: 6.1.1 + eslint: 9.32.0 + is-immutable-type: 5.0.1(eslint@9.32.0)(typescript@5.9.3) + string-ts: 2.3.1 + ts-pattern: 5.9.0 + optionalDependencies: + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + eslint-plugin-unused-imports@3.2.0(eslint@9.32.0): + dependencies: + eslint: 9.32.0 + eslint-rule-composer: 0.3.0 + + eslint-rule-composer@0.3.0: {} + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.32.0: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.32.0) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.3.1 + '@eslint/core': 0.15.2 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.32.0 + '@eslint/plugin-kit': 0.3.5 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + ajv: 6.14.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3(supports-color@5.5.0) + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-util-is-identifier-name@3.0.0: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + esutils@2.0.3: {} + + eventemitter3@4.0.7: {} + + expect-type@1.3.0: {} + + exsolve@1.0.8: {} + + extend@3.0.2: {} + + fast-deep-equal@1.1.0: {} + + fast-deep-equal@3.1.3: {} + + fast-equals@5.4.0: {} + + fast-json-patch@3.1.1: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-safe-stringify@2.1.1: {} + + fast-text-encoding@1.0.6: {} + + fast-uri@3.1.0: {} + + fault@1.0.4: + dependencies: + format: 0.2.2 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-root@1.1.0: {} + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.1 + keyv: 4.5.4 + + flatted@3.4.1: {} + + follow-redirects@1.15.11: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + format@0.2.2: {} + + framer-motion@10.18.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + tslib: 2.8.1 + optionalDependencies: + '@emotion/is-prop-valid': 0.8.8 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + framer-motion@7.10.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@motionone/dom': 10.18.0 + hey-listen: 1.0.8 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.4.0 + optionalDependencies: + '@emotion/is-prop-valid': 0.8.8 + + fs-extra@11.3.4: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs.realpath@1.0.0: {} + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + fuse.js@6.6.2: {} + + generator-function@2.0.1: {} + + gensync@1.0.0-beta.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + get-tsconfig@4.13.6: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@14.0.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + globrex@0.1.2: {} + + good-listener@1.2.2: + dependencies: + delegate: 3.2.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + hammerjs@2.0.8: {} + + handlebars@4.7.8: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + + has-bigints@1.1.0: {} + + has-flag@3.0.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + has@1.0.4: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hast-util-parse-selector@2.2.5: {} + + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hastscript@6.0.0: + dependencies: + '@types/hast': 2.3.10 + comma-separated-tokens: 1.0.8 + hast-util-parse-selector: 2.2.5 + property-information: 5.6.0 + space-separated-tokens: 1.1.5 + + hastscript@9.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + + he@1.2.0: {} + + hey-listen@1.0.8: {} + + highlight.js@10.7.3: {} + + highlight.js@11.11.1: {} + + highlightjs-vue@1.0.0: {} + + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + + html-url-attributes@3.0.1: {} + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + immutable@3.8.3: {} + + immutable@5.1.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-lazy@4.0.0: {} + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + inline-style-parser@0.2.7: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + internmap@2.0.3: {} + + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + + is-alphabetical@1.0.4: {} + + is-alphabetical@2.0.1: {} + + is-alphanumerical@1.0.4: + dependencies: + is-alphabetical: 1.0.4 + is-decimal: 1.0.4 + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-arrayish@0.2.1: {} + + is-arrayish@0.3.4: {} + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-bun-module@2.0.0: + dependencies: + semver: 7.7.4 + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-decimal@1.0.4: {} + + is-decimal@2.0.1: {} + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-hexadecimal@1.0.4: {} + + is-hexadecimal@2.0.1: {} + + is-immutable-type@5.0.1(eslint@9.32.0)(typescript@5.9.3): + dependencies: + '@typescript-eslint/type-utils': 8.57.0(eslint@9.32.0)(typescript@5.9.3) + eslint: 9.32.0 + ts-api-utils: 2.4.0(typescript@5.9.3) + ts-declaration-location: 1.0.7(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-plain-obj@4.1.0: {} + + is-potential-custom-element-name@1.0.1: {} + + is-regex@1.1.4: + dependencies: + call-bind: 1.0.8 + has-tostringtag: 1.0.2 + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.20 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + jju@1.4.0: {} + + jotai@2.18.1(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@18.3.28)(react@18.3.1): + optionalDependencies: + '@babel/core': 7.29.0 + '@babel/template': 7.28.6 + '@types/react': 18.3.28 + react: 18.3.1 + + js-cookie@3.0.5: {} + + js-file-download@0.4.12: {} + + js-sha3@0.8.0: {} + + js-tokens@4.0.0: {} + + js-tokens@9.0.1: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsdom@26.1.0: + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.23 + parse5: 7.3.0 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.19.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + json5@2.2.3: {} + + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsonpath-plus@6.0.1: {} + + keycharm@0.3.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kld-affine@2.1.1: {} + + kld-intersections@0.7.0: + dependencies: + kld-affine: 2.1.1 + kld-path-parser: 0.2.1 + kld-polynomial: 0.3.0 + + kld-path-parser@0.2.1: {} + + kld-polynomial@0.3.0: {} + + kolorist@1.8.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lines-and-columns@1.2.4: {} + + load-script@1.0.0: {} + + local-pkg@1.1.2: + dependencies: + mlly: 1.8.1 + pkg-types: 2.3.0 + quansync: 0.2.11 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.debounce@4.0.8: {} + + lodash.merge@4.6.2: {} + + lodash@4.17.23: {} + + longest-streak@3.1.0: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + loupe@3.2.1: {} + + lower-case@2.0.2: + dependencies: + tslib: 2.8.1 + + lowlight@1.20.0: + dependencies: + fault: 1.0.4 + highlight.js: 10.7.3 + + lru-cache@10.4.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + + luxon@3.7.2: {} + + lz-string@1.5.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + markdown-table@3.0.4: {} + + marked@14.0.0: {} + + match-sorter@6.3.4: + dependencies: + '@babel/runtime': 7.28.6 + remove-accents: 0.5.0 + + math-intrinsics@1.1.0: {} + + mdast-util-directive@3.1.0: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-visit-parents: 6.0.2 + transitivePeerDependencies: + - supports-color + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + memoize-one@5.2.1: {} + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-directive@3.0.2: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + parse-entities: 4.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.12 + debug: 4.4.3(supports-color@5.5.0) + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + + microseconds@0.2.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + min-indent@1.0.1: {} + + minim@0.23.8: + dependencies: + lodash: 4.17.23 + + minimatch@10.2.3: + dependencies: + brace-expansion: 5.0.4 + + minimatch@10.2.4: + dependencies: + brace-expansion: 5.0.4 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + mlly@1.8.1: + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.3 + + mock-json-schema@1.1.2: + dependencies: + lodash: 4.17.23 + + mock-property@1.0.3: + dependencies: + define-data-property: 1.1.4 + functions-have-names: 1.2.3 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + hasown: 2.0.2 + isarray: 2.0.5 + + moment@2.30.1: {} + + monaco-editor@0.55.1: + dependencies: + dompurify: 3.2.7 + marked: 14.0.0 + + monaco-languages-jq@1.0.0: {} + + mousetrap@1.6.5: {} + + ms@2.1.3: {} + + muggle-string@0.4.1: {} + + nano-time@1.0.0: + dependencies: + big-integer: 1.6.52 + + nanoid@3.3.11: {} + + napi-postinstall@0.3.4: {} + + natural-compare@1.4.0: {} + + neo-async@2.6.2: {} + + neotraverse@0.6.18: {} + + next@16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.97.3): + dependencies: + '@next/env': 16.1.6 + '@swc/helpers': 0.5.15 + baseline-browser-mapping: 2.10.0 + caniuse-lite: 1.0.30001777 + postcss: 8.4.31 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + styled-jsx: 5.1.6(@babel/core@7.29.0)(react@18.3.1) + optionalDependencies: + '@next/swc-darwin-arm64': 16.1.6 + '@next/swc-darwin-x64': 16.1.6 + '@next/swc-linux-arm64-gnu': 16.1.6 + '@next/swc-linux-arm64-musl': 16.1.6 + '@next/swc-linux-x64-gnu': 16.1.6 + '@next/swc-linux-x64-musl': 16.1.6 + '@next/swc-win32-arm64-msvc': 16.1.6 + '@next/swc-win32-x64-msvc': 16.1.6 + '@playwright/test': 1.58.2 + sass: 1.97.3 + sharp: 0.34.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + no-case@3.0.4: + dependencies: + lower-case: 2.0.2 + tslib: 2.8.1 + + node-abort-controller@3.1.1: {} + + node-addon-api@7.1.1: + optional: true + + node-addon-api@8.6.0: + optional: true + + node-cache@5.1.2: + dependencies: + clone: 2.1.2 + + node-domexception@1.0.0: {} + + node-fetch-commonjs@3.3.2: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-gyp-build@4.8.4: + optional: true + + node-releases@2.0.36: {} + + nwsapi@2.2.23: {} + + object-assign@4.1.1: {} + + object-inspect@1.12.3: {} + + object-inspect@1.13.4: {} + + object-is@1.1.6: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + oblivious-set@1.0.0: {} + + oblivious-set@1.1.1: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + openapi-path-templating@2.2.1: + dependencies: + apg-lite: 1.0.5 + + openapi-server-url-templating@1.3.0: + dependencies: + apg-lite: 1.0.5 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-cancelable@2.1.1: {} + + p-cancelable@3.0.0: {} + + p-finally@1.0.0: {} + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-queue@6.6.2: + dependencies: + eventemitter3: 4.0.7 + p-timeout: 3.2.0 + + p-timeout@3.2.0: + dependencies: + p-finally: 1.0.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-entities@2.0.0: + dependencies: + character-entities: 1.2.4 + character-entities-legacy: 1.1.4 + character-reference-invalid: 1.1.4 + is-alphanumerical: 1.0.4 + is-decimal: 1.0.4 + is-hexadecimal: 1.0.4 + + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.3.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.0 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse-ms@2.1.0: {} + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + path-browserify@1.0.1: {} + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-to-regexp@8.3.0: {} + + path-type@4.0.0: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.1 + pathe: 2.0.3 + + pkg-types@2.3.0: + dependencies: + confbox: 0.2.4 + exsolve: 1.0.8 + pathe: 2.0.3 + + playwright-core@1.58.2: {} + + playwright@1.58.2: + dependencies: + playwright-core: 1.58.2 + optionalDependencies: + fsevents: 2.3.2 + + popper.js@1.16.1: {} + + possible-typed-array-names@1.1.0: {} + + postcss-value-parser@4.2.0: {} + + postcss@8.4.31: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier@3.8.1: {} + + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + prismjs@1.27.0: {} + + prismjs@1.30.0: {} + + promise-polyfill@8.3.0: {} + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + propagating-hammerjs@1.5.0: + dependencies: + hammerjs: 2.0.8 + + property-expr@2.0.6: {} + + property-information@5.6.0: + dependencies: + xtend: 4.0.2 + + property-information@7.1.0: {} + + proxy-from-env@1.1.0: {} + + punycode@2.3.1: {} + + pvtsutils@1.3.6: + dependencies: + tslib: 2.8.1 + + pvutils@1.1.5: {} + + q@1.4.1: {} + + qs@6.15.0: + dependencies: + side-channel: 1.1.0 + + quansync@0.2.11: {} + + query-state-core@3.1.0: {} + + querystringify@2.2.0: {} + + ramda-adjunct@5.1.0(ramda@0.30.1): + dependencies: + ramda: 0.30.1 + + ramda@0.30.1: {} + + randexp@0.5.3: + dependencies: + drange: 1.1.1 + ret: 0.2.2 + + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + + rdk@6.6.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + body-scroll-lock-upgrade: 1.1.0 + classnames: 2.5.1 + framer-motion: 10.18.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + popper.js: 1.16.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-confetti@6.4.0(react@18.3.1): + dependencies: + react: 18.3.1 + tween-functions: 1.2.0 + + react-container-query@0.12.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + container-query-toolkit: 0.1.3 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + resize-observer-lite: 0.2.3 + + react-cool-dimensions@2.0.7(react@18.3.1): + dependencies: + react: 18.3.1 + + react-copy-to-clipboard@5.1.0(react@18.3.1): + dependencies: + copy-to-clipboard: 3.3.3 + prop-types: 15.8.1 + react: 18.3.1 + + react-data-table-component@7.7.0(react@18.3.1)(styled-components@5.3.11(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react-is@19.2.4)(react@18.3.1)): + dependencies: + deepmerge: 4.3.1 + react: 18.3.1 + styled-components: 5.3.11(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react-is@19.2.4)(react@18.3.1) + + react-datepicker@6.9.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@floating-ui/react': 0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + clsx: 2.1.1 + date-fns: 3.6.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-onclickoutside: 6.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + react-debounce-input@3.3.0(react@18.3.1): + dependencies: + lodash.debounce: 4.0.8 + prop-types: 15.8.1 + react: 18.3.1 + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-fast-compare@3.2.2: {} + + react-helmet@6.1.0(react@18.3.1): + dependencies: + object-assign: 4.1.1 + prop-types: 15.8.1 + react: 18.3.1 + react-fast-compare: 3.2.2 + react-side-effect: 2.1.2(react@18.3.1) + + react-highlight@0.15.0: + dependencies: + highlight.js: 10.7.3 + + react-hook-form@7.71.2(react@18.3.1): + dependencies: + react: 18.3.1 + + react-hotkeys-hook@4.6.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-immutable-proptypes@2.2.0(immutable@3.8.3): + dependencies: + immutable: 3.8.3 + invariant: 2.2.4 + + react-immutable-pure-component@2.2.2(immutable@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + immutable: 3.8.3 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-inspector@6.0.2(react@18.3.1): + dependencies: + react: 18.3.1 + + react-is@16.13.1: {} + + react-is@17.0.2: {} + + react-is@18.3.1: {} + + react-is@19.2.4: {} + + react-markdown@10.1.0(@types/react@18.3.28)(react@18.3.1): + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/react': 18.3.28 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.1 + react: 18.3.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + react-number-format@5.4.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-onclickoutside@6.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-player@2.16.1(react@18.3.1): + dependencies: + deepmerge: 4.3.1 + load-script: 1.0.0 + memoize-one: 5.2.1 + prop-types: 15.8.1 + react: 18.3.1 + react-fast-compare: 3.2.2 + + react-query@3.39.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.6 + broadcast-channel: 3.7.0 + match-sorter: 6.3.4 + react: 18.3.1 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + + react-redux@9.2.0(@types/react@18.3.28)(react@18.3.1)(redux@5.0.1): + dependencies: + '@types/use-sync-external-store': 0.0.6 + react: 18.3.1 + use-sync-external-store: 1.6.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + redux: 5.0.1 + + react-refresh@0.17.0: {} + + react-router-dom@7.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-router: 7.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + react-router-use-location-state@3.1.2(@types/react@18.3.28)(next@16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.97.3))(react-router@7.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-router: 7.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + use-location-state: 3.1.2(@types/react@18.3.28)(next@16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.97.3))(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - next + + react-router@7.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + cookie: 1.1.1 + react: 18.3.1 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + + react-side-effect@2.1.2(react@18.3.1): + dependencies: + react: 18.3.1 + + react-smooth@4.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + fast-equals: 5.4.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + react-syntax-highlighter@15.6.6(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.6 + highlight.js: 10.7.3 + highlightjs-vue: 1.0.0 + lowlight: 1.20.0 + prismjs: 1.30.0 + react: 18.3.1 + refractor: 3.6.0 + + react-syntax-highlighter@16.1.1(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.6 + highlight.js: 10.7.3 + highlightjs-vue: 1.0.0 + lowlight: 1.20.0 + prismjs: 1.30.0 + react: 18.3.1 + refractor: 5.0.0 + + react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.6 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-use-gesture@8.0.1(react@18.3.1): + dependencies: + react: 18.3.1 + + react-vis-timeline@2.0.3(lodash@4.17.23)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@egjs/hammerjs': 2.0.17 + component-emitter: 1.3.1 + keycharm: 0.3.1 + lodash: 4.17.23 + moment: 2.30.1 + propagating-hammerjs: 1.5.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + uuid: 7.0.3 + vis-data: 6.6.1(moment@2.30.1)(uuid@8.3.2)(vis-util@4.3.4) + vis-timeline: 7.3.6(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)(keycharm@0.3.1)(moment@2.30.1)(propagating-hammerjs@1.5.0)(uuid@7.0.3)(vis-data@6.6.1(moment@2.30.1)(uuid@7.0.3)(vis-util@4.3.4))(vis-util@4.3.4) + vis-util: 4.3.4 + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + readdirp@4.1.2: {} + + reaflow@5.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + calculate-size: 1.1.1 + classnames: 2.5.1 + d3-shape: 3.2.0 + elkjs: 0.8.2 + ellipsize: 0.2.0 + framer-motion: 7.10.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + kld-affine: 2.1.1 + kld-intersections: 0.7.0 + p-cancelable: 3.0.0 + rdk: 6.6.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-cool-dimensions: 2.0.7(react@18.3.1) + react-dom: 18.3.1(react@18.3.1) + react-fast-compare: 3.2.2 + react-use-gesture: 8.0.1(react@18.3.1) + reakeys: 1.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + undoo: 0.5.0 + + reakeys@1.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + mousetrap: 1.6.5 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + recharts-scale@0.4.5: + dependencies: + decimal.js-light: 2.5.1 + + recharts@2.15.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + clsx: 2.1.1 + eventemitter3: 4.0.7 + lodash: 4.17.23 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-is: 18.3.1 + react-smooth: 4.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + recharts-scale: 0.4.5 + tiny-invariant: 1.3.3 + victory-vendor: 36.9.2 + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + redux-immutable@4.0.0(immutable@3.8.3): + dependencies: + immutable: 3.8.3 + + redux@5.0.1: {} + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + refractor@3.6.0: + dependencies: + hastscript: 6.0.0 + parse-entities: 2.0.0 + prismjs: 1.27.0 + + refractor@5.0.0: + dependencies: + '@types/hast': 3.0.4 + '@types/prismjs': 1.26.6 + hastscript: 9.0.1 + parse-entities: 4.0.2 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + remark-directive@3.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-directive: 3.1.0 + micromark-extension-directive: 3.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + + remarkable@2.0.1: + dependencies: + argparse: 1.0.10 + autolinker: 3.16.2 + + remove-accents@0.4.4: {} + + remove-accents@0.5.0: {} + + repeat-string@1.6.1: {} + + require-from-string@2.0.2: {} + + requires-port@1.0.0: {} + + reselect@5.1.1: {} + + resize-observer-lite@0.2.3: + dependencies: + element-resize-detector: 1.1.13 + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + ret@0.2.2: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + rollup@4.59.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.59.0 + '@rollup/rollup-android-arm64': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.59.0 + '@rollup/rollup-darwin-x64': 4.59.0 + '@rollup/rollup-freebsd-arm64': 4.59.0 + '@rollup/rollup-freebsd-x64': 4.59.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 + '@rollup/rollup-linux-arm64-musl': 4.59.0 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 + '@rollup/rollup-linux-loong64-musl': 4.59.0 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 + '@rollup/rollup-linux-x64-gnu': 4.59.0 + '@rollup/rollup-linux-x64-musl': 4.59.0 + '@rollup/rollup-openbsd-x64': 4.59.0 + '@rollup/rollup-openharmony-arm64': 4.59.0 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 + '@rollup/rollup-win32-x64-gnu': 4.59.0 + '@rollup/rollup-win32-x64-msvc': 4.59.0 + fsevents: 2.3.3 + + rrweb-cssom@0.8.0: {} + + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-buffer@5.2.1: {} + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safer-buffer@2.1.2: {} + + sass@1.97.3: + dependencies: + chokidar: 4.0.3 + immutable: 5.1.5 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.6 + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + select@1.1.2: {} + + semver@6.3.1: {} + + semver@7.5.4: + dependencies: + lru-cache: 6.0.0 + + semver@7.7.4: {} + + serialize-error@8.1.0: + dependencies: + type-fest: 0.20.2 + + set-cookie-parser@2.7.2: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + + sha.js@2.4.12: + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + to-buffer: 1.2.2 + + shallowequal@1.1.0: {} + + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.7.4 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + optional: true + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + short-unique-id@5.3.2: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + siginfo@2.0.0: {} + + simple-swizzle@0.2.4: + dependencies: + is-arrayish: 0.3.4 + + snake-case@3.0.4: + dependencies: + dot-case: 3.0.4 + tslib: 2.8.1 + + source-map-js@1.2.1: {} + + source-map@0.5.7: {} + + source-map@0.6.1: {} + + space-separated-tokens@1.1.5: {} + + space-separated-tokens@2.0.2: {} + + sprintf-js@1.0.3: {} + + stable-hash@0.0.5: {} + + stackback@0.0.2: {} + + state-local@1.0.7: {} + + std-env@3.10.0: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + string-argv@0.3.2: {} + + string-ts@2.3.1: {} + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + strip-bom@3.0.0: {} + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + strip-json-comments@3.1.1: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + style-to-js@1.1.21: + dependencies: + style-to-object: 1.0.14 + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + + styled-components@5.3.11(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react-is@19.2.4)(react@18.3.1): + dependencies: + '@babel/helper-module-imports': 7.28.6(supports-color@5.5.0) + '@babel/traverse': 7.29.0(supports-color@5.5.0) + '@emotion/is-prop-valid': 1.4.0 + '@emotion/stylis': 0.8.5 + '@emotion/unitless': 0.7.5 + babel-plugin-styled-components: 2.1.4(@babel/core@7.29.0)(styled-components@5.3.11(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react-is@19.2.4)(react@18.3.1))(supports-color@5.5.0) + css-to-react-native: 3.2.0 + hoist-non-react-statics: 3.3.2 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-is: 19.2.4 + shallowequal: 1.1.0 + supports-color: 5.5.0 + transitivePeerDependencies: + - '@babel/core' + + styled-jsx@5.1.6(@babel/core@7.29.0)(react@18.3.1): + dependencies: + client-only: 0.0.1 + react: 18.3.1 + optionalDependencies: + '@babel/core': 7.29.0 + + stylis@4.2.0: {} + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + svg-parser@2.0.4: {} + + swagger-client@3.37.0: + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@scarf/scarf': 1.4.0 + '@swagger-api/apidom-core': 1.6.0 + '@swagger-api/apidom-error': 1.6.0 + '@swagger-api/apidom-json-pointer': 1.6.0 + '@swagger-api/apidom-ns-openapi-3-1': 1.6.0 + '@swagger-api/apidom-ns-openapi-3-2': 1.6.0 + '@swagger-api/apidom-reference': 1.6.0 + '@swaggerexpert/cookie': 2.0.2 + deepmerge: 4.3.1 + fast-json-patch: 3.1.1 + js-yaml: 4.1.1 + neotraverse: 0.6.18 + node-abort-controller: 3.1.1 + node-fetch-commonjs: 3.3.2 + openapi-path-templating: 2.2.1 + openapi-server-url-templating: 1.3.0 + ramda: 0.30.1 + ramda-adjunct: 5.1.0(ramda@0.30.1) + transitivePeerDependencies: + - debug + + swagger-ui-react@5.32.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime-corejs3': 7.29.0 + '@scarf/scarf': 1.4.0 + base64-js: 1.5.1 + buffer: 6.0.3 + classnames: 2.5.1 + css.escape: 1.5.1 + deep-extend: 0.6.0 + dompurify: 3.2.6 + ieee754: 1.2.1 + immutable: 3.8.3 + js-file-download: 0.4.12 + js-yaml: 4.1.1 + lodash: 4.17.23 + prop-types: 15.8.1 + randexp: 0.5.3 + randombytes: 2.1.0 + react: 18.3.1 + react-copy-to-clipboard: 5.1.0(react@18.3.1) + react-debounce-input: 3.3.0(react@18.3.1) + react-dom: 18.3.1(react@18.3.1) + react-immutable-proptypes: 2.2.0(immutable@3.8.3) + react-immutable-pure-component: 2.2.2(immutable@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-inspector: 6.0.2(react@18.3.1) + react-redux: 9.2.0(@types/react@18.3.28)(react@18.3.1)(redux@5.0.1) + react-syntax-highlighter: 16.1.1(react@18.3.1) + redux: 5.0.1 + redux-immutable: 4.0.0(immutable@3.8.3) + remarkable: 2.0.1 + reselect: 5.1.1 + serialize-error: 8.1.0 + sha.js: 2.4.12 + swagger-client: 3.37.0 + url-parse: 1.5.10 + xml: 1.0.1 + xml-but-prettier: 1.0.1 + zenscroll: 4.0.2 + transitivePeerDependencies: + - '@types/react' + - debug + + symbol-tree@3.2.4: {} + + tabbable@6.4.0: {} + + tape@4.17.0: + dependencies: + '@ljharb/resumer': 0.0.1 + '@ljharb/through': 2.3.14 + call-bind: 1.0.8 + deep-equal: 1.1.2 + defined: 1.0.1 + dotignore: 0.1.2 + for-each: 0.3.5 + glob: 7.2.3 + has: 1.0.4 + inherits: 2.0.4 + is-regex: 1.1.4 + minimist: 1.2.8 + mock-property: 1.0.3 + object-inspect: 1.12.3 + resolve: 1.22.11 + string.prototype.trim: 1.2.10 + + text-encoding@0.7.0: {} + + tiny-case@1.0.3: {} + + tiny-emitter@1.1.0: {} + + tiny-emitter@2.1.0: {} + + tiny-invariant@1.3.3: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + + tldts-core@6.1.86: {} + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + + to-buffer@1.2.2: + dependencies: + isarray: 2.0.5 + safe-buffer: 5.2.1 + typed-array-buffer: 1.0.3 + + toggle-selection@1.0.6: {} + + toposort@2.0.2: {} + + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + + tr46@0.0.3: {} + + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + + tree-sitter-json@0.24.8(tree-sitter@0.21.1): + dependencies: + node-addon-api: 8.6.0 + node-gyp-build: 4.8.4 + optionalDependencies: + tree-sitter: 0.21.1 + optional: true + + tree-sitter@0.21.1: + dependencies: + node-addon-api: 8.6.0 + node-gyp-build: 4.8.4 + optional: true + + tree-sitter@0.22.4: + dependencies: + node-addon-api: 8.6.0 + node-gyp-build: 4.8.4 + optional: true + + trim-lines@3.0.1: {} + + trough@2.2.0: {} + + ts-api-utils@2.4.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-declaration-location@1.0.7(typescript@5.9.3): + dependencies: + picomatch: 4.0.3 + typescript: 5.9.3 + + ts-key-enum@2.0.13: {} + + ts-mixer@6.0.4: {} + + ts-pattern@5.9.0: {} + + ts-toolbelt@9.6.0: {} + + tsconfck@3.1.6(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.4.0: {} + + tslib@2.8.1: {} + + tween-functions@1.2.0: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.20.2: {} + + type-fest@2.19.0: {} + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + types-ramda@0.30.1: + dependencies: + ts-toolbelt: 9.6.0 + + typescript-eslint@8.57.0(eslint@9.32.0)(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.57.0(@typescript-eslint/parser@8.57.0(eslint@9.32.0)(typescript@5.9.3))(eslint@9.32.0)(typescript@5.9.3) + '@typescript-eslint/parser': 8.57.0(eslint@9.32.0)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.57.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.0(eslint@9.32.0)(typescript@5.9.3) + eslint: 9.32.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.8.2: {} + + typescript@5.9.3: {} + + u2f-api-polyfill@0.4.3: {} + + ufo@1.6.3: {} + + uglify-js@3.19.3: + optional: true + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + underscore@1.13.1: {} + + undici-types@7.16.0: {} + + undici@7.22.0: + optional: true + + undoo@0.5.0: + dependencies: + defaulty: 2.1.0 + fast-deep-equal: 1.1.0 + + unfetch@4.2.0: {} + + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + universalify@2.0.1: {} + + unload@2.2.0: + dependencies: + '@babel/runtime': 7.28.6 + detect-node: 2.1.0 + + unload@2.3.1: + dependencies: + '@babel/runtime': 7.28.6 + detect-node: 2.1.0 + + unraw@3.0.0: {} + + unrs-resolver@1.11.1: + dependencies: + napi-postinstall: 0.3.4 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.11.1 + '@unrs/resolver-binding-android-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-x64': 1.11.1 + '@unrs/resolver-binding-freebsd-x64': 1.11.1 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 + '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-musl': 1.11.1 + '@unrs/resolver-binding-wasm32-wasi': 1.11.1 + '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 + '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 + '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + + use-isomorphic-layout-effect@1.2.1(@types/react@18.3.28)(react@18.3.1): + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + use-location-state@3.1.2(@types/react@18.3.28)(next@16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.97.3))(react@18.3.1): + dependencies: + '@types/react': 18.3.28 + next: 16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.97.3) + query-state-core: 3.1.0 + react: 18.3.1 + + use-sync-external-store@1.6.0(react@18.3.1): + dependencies: + react: 18.3.1 + + uuid@7.0.3: {} + + uuid@8.3.2: {} + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + victory-vendor@36.9.2: + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + + vis-data@6.6.1(moment@2.30.1)(uuid@8.3.2)(vis-util@4.3.4): + dependencies: + moment: 2.30.1 + uuid: 8.3.2 + vis-util: 4.3.4 + + vis-timeline@7.3.6(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)(keycharm@0.3.1)(moment@2.30.1)(propagating-hammerjs@1.5.0)(uuid@7.0.3)(vis-data@6.6.1(moment@2.30.1)(uuid@7.0.3)(vis-util@4.3.4))(vis-util@4.3.4): + dependencies: + '@egjs/hammerjs': 2.0.17 + component-emitter: 1.3.1 + keycharm: 0.3.1 + moment: 2.30.1 + propagating-hammerjs: 1.5.0 + uuid: 7.0.3 + vis-data: 6.6.1(moment@2.30.1)(uuid@8.3.2)(vis-util@4.3.4) + vis-util: 4.3.4 + + vis-util@4.3.4: {} + + vite-node@3.2.4(@types/node@24.12.0)(sass@1.97.3): + dependencies: + cac: 6.7.14 + debug: 4.4.3(supports-color@5.5.0) + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.1(@types/node@24.12.0)(sass@1.97.3) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite-plugin-dts@4.5.4(@types/node@24.12.0)(rollup@4.59.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.12.0)(sass@1.97.3)): + dependencies: + '@microsoft/api-extractor': 7.57.7(@types/node@24.12.0) + '@rollup/pluginutils': 5.3.0(rollup@4.59.0) + '@volar/typescript': 2.4.28 + '@vue/language-core': 2.2.0(typescript@5.9.3) + compare-versions: 6.1.1 + debug: 4.4.3(supports-color@5.5.0) + kolorist: 1.8.0 + local-pkg: 1.1.2 + magic-string: 0.30.21 + typescript: 5.9.3 + optionalDependencies: + vite: 7.3.1(@types/node@24.12.0)(sass@1.97.3) + transitivePeerDependencies: + - '@types/node' + - rollup + - supports-color + + vite-plugin-svgr@4.5.0(rollup@4.59.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.12.0)(sass@1.97.3)): + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.59.0) + '@svgr/core': 8.1.0(typescript@5.9.3) + '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3)) + vite: 7.3.1(@types/node@24.12.0)(sass@1.97.3) + transitivePeerDependencies: + - rollup + - supports-color + - typescript + + vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.3.1(@types/node@24.12.0)(sass@1.97.3)): + dependencies: + debug: 4.4.3(supports-color@5.5.0) + globrex: 0.1.2 + tsconfck: 3.1.6(typescript@5.9.3) + optionalDependencies: + vite: 7.3.1(@types/node@24.12.0)(sass@1.97.3) + transitivePeerDependencies: + - supports-color + - typescript + + vite@7.3.1(@types/node@24.12.0)(sass@1.97.3): + dependencies: + esbuild: 0.27.3 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.8 + rollup: 4.59.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.12.0 + fsevents: 2.3.3 + sass: 1.97.3 + + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jsdom@26.1.0)(sass@1.97.3): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@24.12.0)(sass@1.97.3)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3(supports-color@5.5.0) + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.1(@types/node@24.12.0)(sass@1.97.3) + vite-node: 3.2.4(@types/node@24.12.0)(sass@1.97.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/debug': 4.1.12 + '@types/node': 24.12.0 + jsdom: 26.1.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vscode-uri@3.1.0: {} + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + web-streams-polyfill@3.3.3: {} + + web-tree-sitter@0.24.5: + optional: true + + webcrypto-core@1.8.1: + dependencies: + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/json-schema': 1.1.12 + asn1js: 3.0.7 + pvtsutils: 1.3.6 + tslib: 2.8.1 + + webcrypto-shim@0.1.7: {} + + webidl-conversions@3.0.1: {} + + webidl-conversions@7.0.0: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.20 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.20: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + wordwrap@1.0.0: {} + + wrappy@1.0.2: {} + + ws@8.19.0: {} + + xhr2@0.1.3: {} + + xml-but-prettier@1.0.1: + dependencies: + repeat-string: 1.6.1 + + xml-name-validator@5.0.0: {} + + xml@1.0.1: {} + + xmlchars@2.2.0: {} + + xstate@4.38.3: {} + + xtend@4.0.2: {} + + yallist@3.1.1: {} + + yallist@4.0.0: {} + + yaml@1.10.2: {} + + yocto-queue@0.1.0: {} + + yup@1.7.1: + dependencies: + property-expr: 2.0.6 + tiny-case: 1.0.3 + toposort: 2.0.2 + type-fest: 2.19.0 + + zenscroll@4.0.2: {} + + zod@4.3.6: {} + + zwitch@2.0.4: {} diff --git a/ui-next/public/conductorLogo-dark.svg b/ui-next/public/conductorLogo-dark.svg new file mode 100644 index 0000000000..4af00ae300 --- /dev/null +++ b/ui-next/public/conductorLogo-dark.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-next/public/conductorLogo.png b/ui-next/public/conductorLogo.png new file mode 100644 index 0000000000..5c05ecbd9d Binary files /dev/null and b/ui-next/public/conductorLogo.png differ diff --git a/ui-next/public/conductorLogo.svg b/ui-next/public/conductorLogo.svg new file mode 100644 index 0000000000..57feb5b0fb --- /dev/null +++ b/ui-next/public/conductorLogo.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-next/public/conductorLogoSmall.png b/ui-next/public/conductorLogoSmall.png new file mode 100644 index 0000000000..f5ecd975ab Binary files /dev/null and b/ui-next/public/conductorLogoSmall.png differ diff --git a/ui-next/public/conductorLogoSmall.svg b/ui-next/public/conductorLogoSmall.svg new file mode 100644 index 0000000000..1cd90c0ca4 --- /dev/null +++ b/ui-next/public/conductorLogoSmall.svg @@ -0,0 +1,52 @@ + + + + + + + + + + diff --git a/ui-next/public/context.js b/ui-next/public/context.js new file mode 100644 index 0000000000..adc7cc1a9e --- /dev/null +++ b/ui-next/public/context.js @@ -0,0 +1,42 @@ +// OSS Conductor UI Runtime Configuration +// This file configures feature flags at runtime for the OSS UI + +window.conductor = { + // Authentication - DISABLED for OSS + ACCESS_MANAGEMENT: false, + RBAC: false, + COPY_TOKEN: false, + + // OSS Core Features + SCHEDULER: true, + TASK_VISIBILITY: "READ", + CREATOR_ENABLE_CREATOR: true, + CREATOR_ENABLE_REAFLOW_DIAGRAM: true, + TASK_INDEXING: false, + SHOW_EVENT_MONITOR: true, + ENABLE_DARK_MODE_TOGGLE: true, + + // Enterprise Features - DISABLED for OSS + WORKFLOW_INTROSPECTION: false, + HUMAN_TASK: false, + INTEGRATIONS: false, + SECRETS: false, + WEBHOOKS: false, + SERVICE_REGISTRY: false, + GATEWAY_ENABLED: false, + REMOTE_SERVICES: false, + SENDGRID_TASK_ENABLED: false, + SKU_ENABLED: false, + + // UI Configuration + PLAYGROUND: false, + ENABLE_METRICS_DASHBOARD: false, + METRICS_ORIGIN_URL: "", + CUSTOM_LOGO_URL: "", + MULTITENANCY_TYPE: "user_based", + DEFAULT_ROLES: "ADMIN", +}; + +// No authentication configuration for OSS +window.authConfig = undefined; +window.auth0Identifiers = undefined; diff --git a/ui-next/public/context.js.example b/ui-next/public/context.js.example new file mode 100644 index 0000000000..73747f4045 --- /dev/null +++ b/ui-next/public/context.js.example @@ -0,0 +1,35 @@ +window.conductor = { + "ENABLE_METRICS_DASHBOARD" : false, + "MULTITENANCY_TYPE" : "none", + "TASK_INDEXING" : "true", + "CREATOR_ENABLE_REAFLOW_DIAGRAM" : "true", + "SHOW_EVENT_MONITOR" : "false", + "ENABLE_NEW_INPUTS" : "true", + "SKU_ENABLED" : "false", + "WEBHOOKS" : "false", + "SERVICE_REGISTRY" : "true", + "SCHEDULER" : "true", + "TASK_VISIBILITY" : "READ", + "SECRETS" : "false", + "INTEGRATIONS" : "true", + "CREATOR_ENABLE_CREATOR" : "false", + "DIAGRAM_DOTTED_BACKGROUND" : "true", + "ENABLE_NEW_SIDEBAR" : "true", + "ACCESS_MANAGEMENT" : true, + "SENDGRID_TASK" : "false", + "COPY_TOKEN" : "true", + "METRICS_ORIGIN_URL" : "false", + "CUSTOM_LOGO_URL" : "", + "RBAC" : "true", + "PLAYGROUND" : "false", + "ENABLE_DRAG_DROP_TASK" : "true", + "HUMAN_TASK" : true, + "DEFAULT_ROLES" : "USER" +}; + +window.authConfig = { + type: "auth0", + domain: "orkes-test.us.auth0.com", + clientId: "wPylSt0D6cJviO3IRFWYHFeyTSldMTWR", + isTestEnvironment: true, +}; diff --git a/ui-next/public/diagramDotBg.svg b/ui-next/public/diagramDotBg.svg new file mode 100644 index 0000000000..487e2c1594 --- /dev/null +++ b/ui-next/public/diagramDotBg.svg @@ -0,0 +1,7493 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-next/public/enterIcon.svg b/ui-next/public/enterIcon.svg new file mode 100644 index 0000000000..48f57e2548 --- /dev/null +++ b/ui-next/public/enterIcon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ui-next/public/enterprise-add-ons/integrations.webp b/ui-next/public/enterprise-add-ons/integrations.webp new file mode 100644 index 0000000000..eac19d464d Binary files /dev/null and b/ui-next/public/enterprise-add-ons/integrations.webp differ diff --git a/ui-next/public/favicon.ico b/ui-next/public/favicon.ico new file mode 100644 index 0000000000..01be939389 Binary files /dev/null and b/ui-next/public/favicon.ico differ diff --git a/ui-next/public/icons/apple-touch-icon.png b/ui-next/public/icons/apple-touch-icon.png new file mode 100644 index 0000000000..b0688f642b Binary files /dev/null and b/ui-next/public/icons/apple-touch-icon.png differ diff --git a/ui-next/public/icons/favicon-16x16.png b/ui-next/public/icons/favicon-16x16.png new file mode 100644 index 0000000000..8a66765f4c Binary files /dev/null and b/ui-next/public/icons/favicon-16x16.png differ diff --git a/ui-next/public/icons/favicon-32x32.png b/ui-next/public/icons/favicon-32x32.png new file mode 100644 index 0000000000..01be939389 Binary files /dev/null and b/ui-next/public/icons/favicon-32x32.png differ diff --git a/ui-next/public/icons/icon-144x144.png b/ui-next/public/icons/icon-144x144.png new file mode 100644 index 0000000000..aa15302b8a Binary files /dev/null and b/ui-next/public/icons/icon-144x144.png differ diff --git a/ui-next/public/icons/icon-192x192.png b/ui-next/public/icons/icon-192x192.png new file mode 100644 index 0000000000..8afcbe14c2 Binary files /dev/null and b/ui-next/public/icons/icon-192x192.png differ diff --git a/ui-next/public/icons/icon-256x256.png b/ui-next/public/icons/icon-256x256.png new file mode 100644 index 0000000000..edae10df5b Binary files /dev/null and b/ui-next/public/icons/icon-256x256.png differ diff --git a/ui-next/public/icons/icon-384x384.png b/ui-next/public/icons/icon-384x384.png new file mode 100644 index 0000000000..034006da87 Binary files /dev/null and b/ui-next/public/icons/icon-384x384.png differ diff --git a/ui-next/public/icons/icon-48x48.png b/ui-next/public/icons/icon-48x48.png new file mode 100644 index 0000000000..00697a941a Binary files /dev/null and b/ui-next/public/icons/icon-48x48.png differ diff --git a/ui-next/public/icons/icon-512x512.png b/ui-next/public/icons/icon-512x512.png new file mode 100644 index 0000000000..4f23c538b8 Binary files /dev/null and b/ui-next/public/icons/icon-512x512.png differ diff --git a/ui-next/public/icons/icon-72x72.png b/ui-next/public/icons/icon-72x72.png new file mode 100644 index 0000000000..249fdd8676 Binary files /dev/null and b/ui-next/public/icons/icon-72x72.png differ diff --git a/ui-next/public/icons/icon-96x96.png b/ui-next/public/icons/icon-96x96.png new file mode 100644 index 0000000000..c76aa4c495 Binary files /dev/null and b/ui-next/public/icons/icon-96x96.png differ diff --git a/ui-next/public/icons/info-icon.svg b/ui-next/public/icons/info-icon.svg new file mode 100644 index 0000000000..0385079013 --- /dev/null +++ b/ui-next/public/icons/info-icon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/ui-next/public/integrations-icons/airtable.svg b/ui-next/public/integrations-icons/airtable.svg new file mode 100644 index 0000000000..adb9cf19bd --- /dev/null +++ b/ui-next/public/integrations-icons/airtable.svg @@ -0,0 +1 @@ +Airtable \ No newline at end of file diff --git a/ui-next/public/integrations-icons/amazon.svg b/ui-next/public/integrations-icons/amazon.svg new file mode 100644 index 0000000000..0941b450b1 --- /dev/null +++ b/ui-next/public/integrations-icons/amazon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/ui-next/public/integrations-icons/amqp.svg b/ui-next/public/integrations-icons/amqp.svg new file mode 100644 index 0000000000..830904d6c4 --- /dev/null +++ b/ui-next/public/integrations-icons/amqp.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/anthropic.svg b/ui-next/public/integrations-icons/anthropic.svg new file mode 100644 index 0000000000..135a8b9f33 --- /dev/null +++ b/ui-next/public/integrations-icons/anthropic.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/apachekafka.svg b/ui-next/public/integrations-icons/apachekafka.svg new file mode 100644 index 0000000000..dc2b7b8bac --- /dev/null +++ b/ui-next/public/integrations-icons/apachekafka.svg @@ -0,0 +1 @@ +Apache Kafka \ No newline at end of file diff --git a/ui-next/public/integrations-icons/asana.svg b/ui-next/public/integrations-icons/asana.svg new file mode 100644 index 0000000000..fcc1674fda --- /dev/null +++ b/ui-next/public/integrations-icons/asana.svg @@ -0,0 +1 @@ +Asana \ No newline at end of file diff --git a/ui-next/public/integrations-icons/aws-lambda.svg b/ui-next/public/integrations-icons/aws-lambda.svg new file mode 100644 index 0000000000..9046496388 --- /dev/null +++ b/ui-next/public/integrations-icons/aws-lambda.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/aws-s3.svg b/ui-next/public/integrations-icons/aws-s3.svg new file mode 100644 index 0000000000..797c2e7e8d --- /dev/null +++ b/ui-next/public/integrations-icons/aws-s3.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/aws-ses.svg b/ui-next/public/integrations-icons/aws-ses.svg new file mode 100644 index 0000000000..f4ad29f54d --- /dev/null +++ b/ui-next/public/integrations-icons/aws-ses.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/aws-sns.svg b/ui-next/public/integrations-icons/aws-sns.svg new file mode 100644 index 0000000000..f3766f02a5 --- /dev/null +++ b/ui-next/public/integrations-icons/aws-sns.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/aws.svg b/ui-next/public/integrations-icons/aws.svg new file mode 100644 index 0000000000..6a7ef0ccdc --- /dev/null +++ b/ui-next/public/integrations-icons/aws.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/azure-devops.svg b/ui-next/public/integrations-icons/azure-devops.svg new file mode 100644 index 0000000000..7dfe84d18c --- /dev/null +++ b/ui-next/public/integrations-icons/azure-devops.svg @@ -0,0 +1 @@ +Icon-devops-261 \ No newline at end of file diff --git a/ui-next/public/integrations-icons/azure-functions.svg b/ui-next/public/integrations-icons/azure-functions.svg new file mode 100644 index 0000000000..1ecacdf37a --- /dev/null +++ b/ui-next/public/integrations-icons/azure-functions.svg @@ -0,0 +1 @@ +Icon-compute-29 \ No newline at end of file diff --git a/ui-next/public/integrations-icons/azure-storage.svg b/ui-next/public/integrations-icons/azure-storage.svg new file mode 100644 index 0000000000..49ebfea9ff --- /dev/null +++ b/ui-next/public/integrations-icons/azure-storage.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/azure.svg b/ui-next/public/integrations-icons/azure.svg new file mode 100644 index 0000000000..7aa96bc980 --- /dev/null +++ b/ui-next/public/integrations-icons/azure.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/ui-next/public/integrations-icons/azureOpenAI.svg b/ui-next/public/integrations-icons/azureOpenAI.svg new file mode 100644 index 0000000000..60cd4418cc --- /dev/null +++ b/ui-next/public/integrations-icons/azureOpenAI.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/azure_openai.svg b/ui-next/public/integrations-icons/azure_openai.svg new file mode 100644 index 0000000000..86dd9e3d39 --- /dev/null +++ b/ui-next/public/integrations-icons/azure_openai.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/ui-next/public/integrations-icons/azure_service_bus.svg b/ui-next/public/integrations-icons/azure_service_bus.svg new file mode 100644 index 0000000000..10975d7216 --- /dev/null +++ b/ui-next/public/integrations-icons/azure_service_bus.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/bedrock.svg b/ui-next/public/integrations-icons/bedrock.svg new file mode 100644 index 0000000000..3bca424ed7 --- /dev/null +++ b/ui-next/public/integrations-icons/bedrock.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/bitbucket.svg b/ui-next/public/integrations-icons/bitbucket.svg new file mode 100644 index 0000000000..20cfca483a --- /dev/null +++ b/ui-next/public/integrations-icons/bitbucket.svg @@ -0,0 +1 @@ +Bitbucket \ No newline at end of file diff --git a/ui-next/public/integrations-icons/circleci.svg b/ui-next/public/integrations-icons/circleci.svg new file mode 100644 index 0000000000..6e69e373ed --- /dev/null +++ b/ui-next/public/integrations-icons/circleci.svg @@ -0,0 +1 @@ +CircleCI \ No newline at end of file diff --git a/ui-next/public/integrations-icons/cloudflare.svg b/ui-next/public/integrations-icons/cloudflare.svg new file mode 100644 index 0000000000..66cc020865 --- /dev/null +++ b/ui-next/public/integrations-icons/cloudflare.svg @@ -0,0 +1 @@ +Cloudflare \ No newline at end of file diff --git a/ui-next/public/integrations-icons/cohere.svg b/ui-next/public/integrations-icons/cohere.svg new file mode 100644 index 0000000000..543bc2d6ca --- /dev/null +++ b/ui-next/public/integrations-icons/cohere.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/commonroom.svg b/ui-next/public/integrations-icons/commonroom.svg new file mode 100644 index 0000000000..4a93b4196e --- /dev/null +++ b/ui-next/public/integrations-icons/commonroom.svg @@ -0,0 +1,8 @@ + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/conductor.svg b/ui-next/public/integrations-icons/conductor.svg new file mode 100644 index 0000000000..4e6767fba9 --- /dev/null +++ b/ui-next/public/integrations-icons/conductor.svg @@ -0,0 +1,9 @@ + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/confluent.svg b/ui-next/public/integrations-icons/confluent.svg new file mode 100644 index 0000000000..d6c16c2196 --- /dev/null +++ b/ui-next/public/integrations-icons/confluent.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/ui-next/public/integrations-icons/datadog.svg b/ui-next/public/integrations-icons/datadog.svg new file mode 100644 index 0000000000..ab43731901 --- /dev/null +++ b/ui-next/public/integrations-icons/datadog.svg @@ -0,0 +1 @@ +Datadog \ No newline at end of file diff --git a/ui-next/public/integrations-icons/default.svg b/ui-next/public/integrations-icons/default.svg new file mode 100644 index 0000000000..133b576d6a --- /dev/null +++ b/ui-next/public/integrations-icons/default.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/digitalocean.svg b/ui-next/public/integrations-icons/digitalocean.svg new file mode 100644 index 0000000000..9d5750296c --- /dev/null +++ b/ui-next/public/integrations-icons/digitalocean.svg @@ -0,0 +1 @@ +DigitalOcean \ No newline at end of file diff --git a/ui-next/public/integrations-icons/discord.svg b/ui-next/public/integrations-icons/discord.svg new file mode 100644 index 0000000000..ef25142a35 --- /dev/null +++ b/ui-next/public/integrations-icons/discord.svg @@ -0,0 +1 @@ +Discord \ No newline at end of file diff --git a/ui-next/public/integrations-icons/discourse.svg b/ui-next/public/integrations-icons/discourse.svg new file mode 100644 index 0000000000..4cbb8c87b3 --- /dev/null +++ b/ui-next/public/integrations-icons/discourse.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/docker.svg b/ui-next/public/integrations-icons/docker.svg new file mode 100644 index 0000000000..f1b97ba55b --- /dev/null +++ b/ui-next/public/integrations-icons/docker.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/freshdesk.svg b/ui-next/public/integrations-icons/freshdesk.svg new file mode 100644 index 0000000000..ecb5c6e0c6 --- /dev/null +++ b/ui-next/public/integrations-icons/freshdesk.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/gcp_pubsub.svg b/ui-next/public/integrations-icons/gcp_pubsub.svg new file mode 100644 index 0000000000..b200409185 --- /dev/null +++ b/ui-next/public/integrations-icons/gcp_pubsub.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/gemini.svg b/ui-next/public/integrations-icons/gemini.svg new file mode 100644 index 0000000000..3c03da6610 --- /dev/null +++ b/ui-next/public/integrations-icons/gemini.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/ui-next/public/integrations-icons/git.svg b/ui-next/public/integrations-icons/git.svg new file mode 100644 index 0000000000..5f19a87f75 --- /dev/null +++ b/ui-next/public/integrations-icons/git.svg @@ -0,0 +1 @@ +Git \ No newline at end of file diff --git a/ui-next/public/integrations-icons/github.svg b/ui-next/public/integrations-icons/github.svg new file mode 100644 index 0000000000..a8d1174049 --- /dev/null +++ b/ui-next/public/integrations-icons/github.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui-next/public/integrations-icons/gitlab.svg b/ui-next/public/integrations-icons/gitlab.svg new file mode 100644 index 0000000000..52b926cc70 --- /dev/null +++ b/ui-next/public/integrations-icons/gitlab.svg @@ -0,0 +1,22 @@ + + + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/gmail.svg b/ui-next/public/integrations-icons/gmail.svg new file mode 100644 index 0000000000..6c9a3c870e --- /dev/null +++ b/ui-next/public/integrations-icons/gmail.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-next/public/integrations-icons/google-cloud-functions.svg b/ui-next/public/integrations-icons/google-cloud-functions.svg new file mode 100644 index 0000000000..0429220013 --- /dev/null +++ b/ui-next/public/integrations-icons/google-cloud-functions.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/google-cloud-storage.svg b/ui-next/public/integrations-icons/google-cloud-storage.svg new file mode 100644 index 0000000000..842c121a7b --- /dev/null +++ b/ui-next/public/integrations-icons/google-cloud-storage.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/google-docs.svg b/ui-next/public/integrations-icons/google-docs.svg new file mode 100644 index 0000000000..7b7cb14e08 --- /dev/null +++ b/ui-next/public/integrations-icons/google-docs.svg @@ -0,0 +1,88 @@ + + + Docs-icon + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/google-drive.svg b/ui-next/public/integrations-icons/google-drive.svg new file mode 100644 index 0000000000..a8cefd5b28 --- /dev/null +++ b/ui-next/public/integrations-icons/google-drive.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/google-sheets.svg b/ui-next/public/integrations-icons/google-sheets.svg new file mode 100644 index 0000000000..bd5d938c78 --- /dev/null +++ b/ui-next/public/integrations-icons/google-sheets.svg @@ -0,0 +1,89 @@ + + + + Sheets-icon + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/google-slides.svg b/ui-next/public/integrations-icons/google-slides.svg new file mode 100644 index 0000000000..deeb2482a9 --- /dev/null +++ b/ui-next/public/integrations-icons/google-slides.svg @@ -0,0 +1,96 @@ + + + Slides-icon + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/google_sheets.svg b/ui-next/public/integrations-icons/google_sheets.svg new file mode 100644 index 0000000000..7eaeadd011 --- /dev/null +++ b/ui-next/public/integrations-icons/google_sheets.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-next/public/integrations-icons/googleads.svg b/ui-next/public/integrations-icons/googleads.svg new file mode 100644 index 0000000000..e098e77754 --- /dev/null +++ b/ui-next/public/integrations-icons/googleads.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/googleanalytics.svg b/ui-next/public/integrations-icons/googleanalytics.svg new file mode 100644 index 0000000000..d98900882b --- /dev/null +++ b/ui-next/public/integrations-icons/googleanalytics.svg @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/googlecalendar.svg b/ui-next/public/integrations-icons/googlecalendar.svg new file mode 100644 index 0000000000..91f0e23104 --- /dev/null +++ b/ui-next/public/integrations-icons/googlecalendar.svg @@ -0,0 +1 @@ +Google Calendar \ No newline at end of file diff --git a/ui-next/public/integrations-icons/googledrive.svg b/ui-next/public/integrations-icons/googledrive.svg new file mode 100644 index 0000000000..7263ef3124 --- /dev/null +++ b/ui-next/public/integrations-icons/googledrive.svg @@ -0,0 +1 @@ +Google Drive \ No newline at end of file diff --git a/ui-next/public/integrations-icons/googlegemini.svg b/ui-next/public/integrations-icons/googlegemini.svg new file mode 100644 index 0000000000..e15e53ce01 --- /dev/null +++ b/ui-next/public/integrations-icons/googlegemini.svg @@ -0,0 +1 @@ +Google Gemini \ No newline at end of file diff --git a/ui-next/public/integrations-icons/grok.svg b/ui-next/public/integrations-icons/grok.svg new file mode 100644 index 0000000000..c5736c1dc9 --- /dev/null +++ b/ui-next/public/integrations-icons/grok.svg @@ -0,0 +1 @@ +Grok \ No newline at end of file diff --git a/ui-next/public/integrations-icons/hubspot.svg b/ui-next/public/integrations-icons/hubspot.svg new file mode 100644 index 0000000000..0ed8b1d3e8 --- /dev/null +++ b/ui-next/public/integrations-icons/hubspot.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/huggingFace.svg b/ui-next/public/integrations-icons/huggingFace.svg new file mode 100644 index 0000000000..7d70fe5b7b --- /dev/null +++ b/ui-next/public/integrations-icons/huggingFace.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/ibm_mq.svg b/ui-next/public/integrations-icons/ibm_mq.svg new file mode 100644 index 0000000000..e72268177f --- /dev/null +++ b/ui-next/public/integrations-icons/ibm_mq.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/instaclustr.svg b/ui-next/public/integrations-icons/instaclustr.svg new file mode 100644 index 0000000000..6268e9e739 --- /dev/null +++ b/ui-next/public/integrations-icons/instaclustr.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/intercom.svg b/ui-next/public/integrations-icons/intercom.svg new file mode 100644 index 0000000000..cce4b72cd0 --- /dev/null +++ b/ui-next/public/integrations-icons/intercom.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/jira.svg b/ui-next/public/integrations-icons/jira.svg new file mode 100644 index 0000000000..841a768230 --- /dev/null +++ b/ui-next/public/integrations-icons/jira.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-next/public/integrations-icons/kafka.svg b/ui-next/public/integrations-icons/kafka.svg new file mode 100644 index 0000000000..d041b8d7a1 --- /dev/null +++ b/ui-next/public/integrations-icons/kafka.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/kafka_confluent.svg b/ui-next/public/integrations-icons/kafka_confluent.svg new file mode 100644 index 0000000000..0b821f23fa --- /dev/null +++ b/ui-next/public/integrations-icons/kafka_confluent.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/kafka_msk.svg b/ui-next/public/integrations-icons/kafka_msk.svg new file mode 100644 index 0000000000..2be58c7679 --- /dev/null +++ b/ui-next/public/integrations-icons/kafka_msk.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/ui-next/public/integrations-icons/kubernetes.svg b/ui-next/public/integrations-icons/kubernetes.svg new file mode 100644 index 0000000000..4e52a4bc46 --- /dev/null +++ b/ui-next/public/integrations-icons/kubernetes.svg @@ -0,0 +1,19 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/linear.svg b/ui-next/public/integrations-icons/linear.svg new file mode 100644 index 0000000000..9ac4481309 --- /dev/null +++ b/ui-next/public/integrations-icons/linear.svg @@ -0,0 +1 @@ +Linear \ No newline at end of file diff --git a/ui-next/public/integrations-icons/mailgun.svg b/ui-next/public/integrations-icons/mailgun.svg new file mode 100644 index 0000000000..e18cc4a260 --- /dev/null +++ b/ui-next/public/integrations-icons/mailgun.svg @@ -0,0 +1 @@ +Mailgun \ No newline at end of file diff --git a/ui-next/public/integrations-icons/menuBook.svg b/ui-next/public/integrations-icons/menuBook.svg new file mode 100644 index 0000000000..41351de629 --- /dev/null +++ b/ui-next/public/integrations-icons/menuBook.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/mistral.svg b/ui-next/public/integrations-icons/mistral.svg new file mode 100644 index 0000000000..348ec9c081 --- /dev/null +++ b/ui-next/public/integrations-icons/mistral.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/mistralai.svg b/ui-next/public/integrations-icons/mistralai.svg new file mode 100644 index 0000000000..00ee1b9e51 --- /dev/null +++ b/ui-next/public/integrations-icons/mistralai.svg @@ -0,0 +1 @@ +Mistral AI \ No newline at end of file diff --git a/ui-next/public/integrations-icons/mixpanel.svg b/ui-next/public/integrations-icons/mixpanel.svg new file mode 100644 index 0000000000..8ebc7d08b2 --- /dev/null +++ b/ui-next/public/integrations-icons/mixpanel.svg @@ -0,0 +1 @@ +Mixpanel \ No newline at end of file diff --git a/ui-next/public/integrations-icons/mongo.svg b/ui-next/public/integrations-icons/mongo.svg new file mode 100644 index 0000000000..22474a2578 --- /dev/null +++ b/ui-next/public/integrations-icons/mongo.svg @@ -0,0 +1,2 @@ + +MongoDB \ No newline at end of file diff --git a/ui-next/public/integrations-icons/mongodb.svg b/ui-next/public/integrations-icons/mongodb.svg new file mode 100644 index 0000000000..b7db5d8267 --- /dev/null +++ b/ui-next/public/integrations-icons/mongodb.svg @@ -0,0 +1,26 @@ + + + databases-and-servers/databases/mongodb + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/mongovector.svg b/ui-next/public/integrations-icons/mongovector.svg new file mode 100644 index 0000000000..8460916be3 --- /dev/null +++ b/ui-next/public/integrations-icons/mongovector.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/mysql.svg b/ui-next/public/integrations-icons/mysql.svg new file mode 100644 index 0000000000..06449afc4a --- /dev/null +++ b/ui-next/public/integrations-icons/mysql.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/nats.svg b/ui-next/public/integrations-icons/nats.svg new file mode 100644 index 0000000000..2085f20e52 --- /dev/null +++ b/ui-next/public/integrations-icons/nats.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/notion.svg b/ui-next/public/integrations-icons/notion.svg new file mode 100644 index 0000000000..201f7bb6dc --- /dev/null +++ b/ui-next/public/integrations-icons/notion.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/ui-next/public/integrations-icons/okta.svg b/ui-next/public/integrations-icons/okta.svg new file mode 100644 index 0000000000..f9a4633850 --- /dev/null +++ b/ui-next/public/integrations-icons/okta.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/ollama.svg b/ui-next/public/integrations-icons/ollama.svg new file mode 100644 index 0000000000..432f73e738 --- /dev/null +++ b/ui-next/public/integrations-icons/ollama.svg @@ -0,0 +1 @@ +Ollama \ No newline at end of file diff --git a/ui-next/public/integrations-icons/openAI.svg b/ui-next/public/integrations-icons/openAI.svg new file mode 100644 index 0000000000..60cd4418cc --- /dev/null +++ b/ui-next/public/integrations-icons/openAI.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/pagerduty.svg b/ui-next/public/integrations-icons/pagerduty.svg new file mode 100644 index 0000000000..f9dffb1e0a --- /dev/null +++ b/ui-next/public/integrations-icons/pagerduty.svg @@ -0,0 +1 @@ +PagerDuty \ No newline at end of file diff --git a/ui-next/public/integrations-icons/perplexity.svg b/ui-next/public/integrations-icons/perplexity.svg new file mode 100644 index 0000000000..38addf13c1 Binary files /dev/null and b/ui-next/public/integrations-icons/perplexity.svg differ diff --git a/ui-next/public/integrations-icons/pgvector.svg b/ui-next/public/integrations-icons/pgvector.svg new file mode 100644 index 0000000000..f290ec44f7 --- /dev/null +++ b/ui-next/public/integrations-icons/pgvector.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/pinecone.svg b/ui-next/public/integrations-icons/pinecone.svg new file mode 100644 index 0000000000..2f5f7f4f21 --- /dev/null +++ b/ui-next/public/integrations-icons/pinecone.svg @@ -0,0 +1,6 @@ + + Pinecone + + + + diff --git a/ui-next/public/integrations-icons/pipedrive.svg b/ui-next/public/integrations-icons/pipedrive.svg new file mode 100644 index 0000000000..70d590aec4 --- /dev/null +++ b/ui-next/public/integrations-icons/pipedrive.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/postgres.svg b/ui-next/public/integrations-icons/postgres.svg new file mode 100644 index 0000000000..709b5c1114 --- /dev/null +++ b/ui-next/public/integrations-icons/postgres.svg @@ -0,0 +1,2 @@ + +PostgreSQL \ No newline at end of file diff --git a/ui-next/public/integrations-icons/private-ai.svg b/ui-next/public/integrations-icons/private-ai.svg new file mode 100644 index 0000000000..5336cd36e0 --- /dev/null +++ b/ui-next/public/integrations-icons/private-ai.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/rabbitmq.svg b/ui-next/public/integrations-icons/rabbitmq.svg new file mode 100644 index 0000000000..990c6d42ce --- /dev/null +++ b/ui-next/public/integrations-icons/rabbitmq.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/redis.svg b/ui-next/public/integrations-icons/redis.svg new file mode 100644 index 0000000000..c2834d260f --- /dev/null +++ b/ui-next/public/integrations-icons/redis.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/relational_db.svg b/ui-next/public/integrations-icons/relational_db.svg new file mode 100644 index 0000000000..ebb2849812 --- /dev/null +++ b/ui-next/public/integrations-icons/relational_db.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/sendgrid.svg b/ui-next/public/integrations-icons/sendgrid.svg new file mode 100644 index 0000000000..53e44f09f9 --- /dev/null +++ b/ui-next/public/integrations-icons/sendgrid.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/sentry.svg b/ui-next/public/integrations-icons/sentry.svg new file mode 100644 index 0000000000..60a0746893 --- /dev/null +++ b/ui-next/public/integrations-icons/sentry.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/slack.svg b/ui-next/public/integrations-icons/slack.svg new file mode 100644 index 0000000000..60518a6736 --- /dev/null +++ b/ui-next/public/integrations-icons/slack.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/ui-next/public/integrations-icons/stripe.svg b/ui-next/public/integrations-icons/stripe.svg new file mode 100644 index 0000000000..9e21c18c33 --- /dev/null +++ b/ui-next/public/integrations-icons/stripe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/symphone.svg b/ui-next/public/integrations-icons/symphone.svg new file mode 100644 index 0000000000..793a3b8ef1 --- /dev/null +++ b/ui-next/public/integrations-icons/symphone.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/teams.svg b/ui-next/public/integrations-icons/teams.svg new file mode 100644 index 0000000000..353c07d054 --- /dev/null +++ b/ui-next/public/integrations-icons/teams.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/ui-next/public/integrations-icons/telegram.svg b/ui-next/public/integrations-icons/telegram.svg new file mode 100644 index 0000000000..b637cb5160 --- /dev/null +++ b/ui-next/public/integrations-icons/telegram.svg @@ -0,0 +1 @@ +Telegram \ No newline at end of file diff --git a/ui-next/public/integrations-icons/terraform.svg b/ui-next/public/integrations-icons/terraform.svg new file mode 100644 index 0000000000..536afddac0 --- /dev/null +++ b/ui-next/public/integrations-icons/terraform.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/trello.svg b/ui-next/public/integrations-icons/trello.svg new file mode 100644 index 0000000000..6b5c9e27f4 --- /dev/null +++ b/ui-next/public/integrations-icons/trello.svg @@ -0,0 +1,2 @@ + +Trello \ No newline at end of file diff --git a/ui-next/public/integrations-icons/twilio.svg b/ui-next/public/integrations-icons/twilio.svg new file mode 100644 index 0000000000..2392f4bd91 --- /dev/null +++ b/ui-next/public/integrations-icons/twilio.svg @@ -0,0 +1 @@ +Twilio \ No newline at end of file diff --git a/ui-next/public/integrations-icons/vertexAI.svg b/ui-next/public/integrations-icons/vertexAI.svg new file mode 100644 index 0000000000..a350af8e2b --- /dev/null +++ b/ui-next/public/integrations-icons/vertexAI.svg @@ -0,0 +1,285 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/weaviate.svg b/ui-next/public/integrations-icons/weaviate.svg new file mode 100644 index 0000000000..c21035957b --- /dev/null +++ b/ui-next/public/integrations-icons/weaviate.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/ui-next/public/integrations-icons/youtube.svg b/ui-next/public/integrations-icons/youtube.svg new file mode 100644 index 0000000000..9f85f42217 --- /dev/null +++ b/ui-next/public/integrations-icons/youtube.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/ui-next/public/integrations-icons/zendesk.svg b/ui-next/public/integrations-icons/zendesk.svg new file mode 100644 index 0000000000..72632201e7 --- /dev/null +++ b/ui-next/public/integrations-icons/zendesk.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-next/public/logo.png b/ui-next/public/logo.png new file mode 100644 index 0000000000..a5997590d5 Binary files /dev/null and b/ui-next/public/logo.png differ diff --git a/ui-next/public/orkes-logo-purple-2x.png b/ui-next/public/orkes-logo-purple-2x.png new file mode 100644 index 0000000000..eb4539d934 Binary files /dev/null and b/ui-next/public/orkes-logo-purple-2x.png differ diff --git a/ui-next/public/orkes-logo-purple-inverted-2x.png b/ui-next/public/orkes-logo-purple-inverted-2x.png new file mode 100644 index 0000000000..f844d44623 Binary files /dev/null and b/ui-next/public/orkes-logo-purple-inverted-2x.png differ diff --git a/ui-next/public/programming-language-icons/python.svg b/ui-next/public/programming-language-icons/python.svg new file mode 100644 index 0000000000..f742e7d6ff --- /dev/null +++ b/ui-next/public/programming-language-icons/python.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ui-next/public/robots.txt b/ui-next/public/robots.txt new file mode 100644 index 0000000000..e9e57dc4d4 --- /dev/null +++ b/ui-next/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/ui-next/public/searchIconBg.svg b/ui-next/public/searchIconBg.svg new file mode 100644 index 0000000000..2be345446a --- /dev/null +++ b/ui-next/public/searchIconBg.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ui-next/public/wh-icons/github-icon.svg b/ui-next/public/wh-icons/github-icon.svg new file mode 100644 index 0000000000..b02cc459e8 --- /dev/null +++ b/ui-next/public/wh-icons/github-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/ui-next/public/wh-icons/microsoft-teams-icon.svg b/ui-next/public/wh-icons/microsoft-teams-icon.svg new file mode 100644 index 0000000000..e88affb181 --- /dev/null +++ b/ui-next/public/wh-icons/microsoft-teams-icon.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-next/public/wh-icons/send-grid-icon.svg b/ui-next/public/wh-icons/send-grid-icon.svg new file mode 100644 index 0000000000..9450d5f1d0 --- /dev/null +++ b/ui-next/public/wh-icons/send-grid-icon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/ui-next/public/wh-icons/slack-icon.svg b/ui-next/public/wh-icons/slack-icon.svg new file mode 100644 index 0000000000..4b995c9aa2 --- /dev/null +++ b/ui-next/public/wh-icons/slack-icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/ui-next/public/wh-icons/stripe-icon.svg b/ui-next/public/wh-icons/stripe-icon.svg new file mode 100644 index 0000000000..3b95e2b531 --- /dev/null +++ b/ui-next/public/wh-icons/stripe-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui-next/src/commonServices/execution.ts b/ui-next/src/commonServices/execution.ts new file mode 100644 index 0000000000..ebc851e0c0 --- /dev/null +++ b/ui-next/src/commonServices/execution.ts @@ -0,0 +1,37 @@ +import { queryClient } from "queryClient"; +import { fetchWithContext, fetchContextNonHook } from "plugins/fetch"; +import { getErrors } from "../utils/utils"; +import { HasAuthHeaders } from "types/common"; +import { featureFlags, FEATURES } from "utils/flags"; + +const fetchContext = fetchContextNonHook(); +const isWorkflowIntrospectionEnabled = featureFlags.isEnabled( + FEATURES.WORKFLOW_INTROSPECTION, +); + +export const fetchExecution = async ({ + authHeaders: headers, + executionId, +}: HasAuthHeaders & { executionId: string }) => { + const url = `/workflow/${executionId}?summarize=true`; + const introspectionUrl = `/workflow/introspection/records?workflowId=${executionId}`; + + try { + const workflowExecution = await queryClient.fetchQuery( + [fetchContext.stack, url], + () => fetchWithContext(url, fetchContext, { headers }), + ); + + if (isWorkflowIntrospectionEnabled) { + workflowExecution.workflowIntrospection = await queryClient.fetchQuery( + [fetchContext.stack, introspectionUrl], + () => fetchWithContext(introspectionUrl, fetchContext, { headers }), + ); + } + + return workflowExecution; + } catch (error) { + const errorDetails = await getErrors(error as Response); + return Promise.reject({ originalError: error, errorDetails }); + } +}; diff --git a/ui-next/src/commonServices/index.ts b/ui-next/src/commonServices/index.ts new file mode 100644 index 0000000000..9e2d798e6c --- /dev/null +++ b/ui-next/src/commonServices/index.ts @@ -0,0 +1 @@ +export * from "./execution"; diff --git a/ui-next/src/components/ActionButton.tsx b/ui-next/src/components/ActionButton.tsx new file mode 100644 index 0000000000..3cd99b3058 --- /dev/null +++ b/ui-next/src/components/ActionButton.tsx @@ -0,0 +1,53 @@ +import { Box } from "@mui/material"; +import CircularProgress from "@mui/material/CircularProgress"; +import Button, { MuiButtonProps } from "components/MuiButton"; + +const style = { + root: { + display: "inline-flex", + flexDirection: "column", + alignItems: "flex-start", + }, + wrapper: { + position: "relative", + width: "100%", + }, + buttonProgress: { + position: "absolute", + top: "50%", + left: "50%", + marginTop: "-12px", + marginLeft: "-12px", + }, +}; + +export interface IActionButtonProps extends MuiButtonProps { + progress?: boolean; +} + +const ActionButton = ({ + children, + disabled, + onClick, + progress, + ...props +}: IActionButtonProps) => { + return ( + + + + {progress && ( + + )} + + + ); +}; + +export default ActionButton; diff --git a/ui-next/src/components/App.tsx b/ui-next/src/components/App.tsx new file mode 100644 index 0000000000..3f0cea5892 --- /dev/null +++ b/ui-next/src/components/App.tsx @@ -0,0 +1,108 @@ +import { SafariWarning } from "components/SafariWarning"; +import OnboardingQuiz from "components/v1/quiz/OnboardingQuiz"; +import React, { useState } from "react"; +import { Helmet } from "react-helmet"; +import { Outlet } from "react-router"; +import { AuthProvider as AuthProviderImport } from "shared/auth/AuthProvider"; +import SideAndTopBarsLayout from "shared/SideAndTopBarsLayout"; +import { SidebarProvider } from "components/Sidebar/context/SidebarContextProvider"; +import { UserSettingsProvider } from "shared/UserSettingsProvider"; +import { pluginRegistry } from "plugins/registry"; +import { + featureFlags, + FEATURES, + GTAG_LABEL, + isSafari, + useAPIReleaseVersion, + useMaybeEnableLogRocket, +} from "utils"; +import { getThemeAsCSSVariables } from "utils/themeVariables"; + +// Resolve global components once at module load time (after plugins are registered) +const globalComponents = pluginRegistry.getGlobalComponents(); + +const AuthProvider = AuthProviderImport as React.ComponentType<{ + children: React.ReactNode; +}>; + +const showOnboardingQuiz = featureFlags.isEnabled( + FEATURES.SHOW_ONBOARDING_QUIZ, +); + +const isPlayground = featureFlags.isEnabled(FEATURES.PLAYGROUND); + +// App component that will be used as the root element +export function App() { + useAPIReleaseVersion({ option: { enabled: true } }); + useMaybeEnableLogRocket(); + + // Checking responsive width (Mobile) + const [showSafariWarning, setShowSafariWarning] = useState(isSafari); + + const themeAsCSSVariables = getThemeAsCSSVariables(); + + return ( + + + + + {showOnboardingQuiz ? : null} + + + + {showSafariWarning && ( + + )} + + + + + {/* Global plugin components (e.g. pollers, invisible side-effect components) */} + {globalComponents.map((Component, i) => ( + + ))} + {isPlayground ? ( + + + + + ) : null} + + + ); +} diff --git a/ui-next/src/components/AutoCompleteWithDescription.tsx b/ui-next/src/components/AutoCompleteWithDescription.tsx new file mode 100644 index 0000000000..851f103f58 --- /dev/null +++ b/ui-next/src/components/AutoCompleteWithDescription.tsx @@ -0,0 +1,102 @@ +import { CSSProperties, FunctionComponent, ReactNode } from "react"; +import Autocomplete, { createFilterOptions } from "@mui/material/Autocomplete"; +import Box from "@mui/material/Box"; +import { fontWeights } from "theme/tokens/variables"; +import TextField from "@mui/material/TextField"; +import { Popper } from "@mui/material"; + +const filter = createFilterOptions(); + +interface AutoCompleteWithDescriptionProps { + value?: string; + options?: { name: string; description: ReactNode }[]; + error?: boolean; + helperText?: string; + onChange: (value: string) => void; + placeholder?: string; + growPopper?: boolean; +} + +export const AutoCompleteWithDescription: FunctionComponent< + AutoCompleteWithDescriptionProps +> = ({ + value, + options = [], + error = false, + helperText, + onChange, + placeholder = "", + growPopper, +}) => { + const popperStyle = (style: CSSProperties | undefined) => { + return growPopper ? { maxWidth: "300px" } : style; + }; + return ( + ( + + )} + value={value ? value : ""} + isOptionEqualToValue={(option: any, currentValue: any) => + option?.name === currentValue + } + autoHighlight + componentsProps={{ paper: { elevation: 3 } }} + onChange={(_event, newValue: any) => { + onChange(newValue?.name); + }} + filterOptions={(options, params) => { + const filtered = filter(options, params); + return filtered; + }} + id="assignment-type-dialog" + options={options} + getOptionLabel={(option) => { + // e.g value selected with enter, right from the input + if (typeof option === "string") { + return option; + } + return option?.name; + }} + selectOnFocus + clearOnBlur + handleHomeEndKeys + renderOption={(props, option) => ( +
  • + + + {option.name} + + {option.description} + +
  • + )} + freeSolo + renderInput={(params) => ( + + )} + /> + ); +}; diff --git a/ui-next/src/components/AutoRefreshButton.tsx b/ui-next/src/components/AutoRefreshButton.tsx new file mode 100644 index 0000000000..3d4a361089 --- /dev/null +++ b/ui-next/src/components/AutoRefreshButton.tsx @@ -0,0 +1,175 @@ +import { + IconProps, + ArrowCounterClockwise as Restart, +} from "@phosphor-icons/react"; +import { useActor, useSelector } from "@xstate/react"; +import RefreshIcon from "components/v1/icons/RefreshIcon"; +import isNil from "lodash/isNil"; +import { + COUNT_DOWN_TYPE, + CountdownContext, + CountdownEventTypes, + CountdownEvents, +} from "pages/execution/state/types"; +import { + ForwardRefExoticComponent, + FunctionComponent, + RefAttributes, + useState, +} from "react"; +import { WorkflowExecutionStatus } from "types/Execution"; +import { ActorRef, State } from "xstate"; +import DropdownButton from "./DropdownButton"; +import Button, { MuiButtonProps } from "./MuiButton"; +import { SpinningIcon } from "./v1/SpinningIcon"; + +interface AutoRefreshButtonProps { + buttonProps: MuiButtonProps; + countdownActor: ActorRef; +} + +const SpinningRefreshIcon = SpinningIcon( + RefreshIcon as ForwardRefExoticComponent< + IconProps & RefAttributes + >, +); + +const AutoRefreshButton: FunctionComponent = ({ + buttonProps, + countdownActor, +}) => { + const duration = useSelector( + countdownActor, + (state: State) => state.context.duration, + ); + const elapsed = useSelector(countdownActor, (state) => state.context.elapsed); + const isDisabled = useSelector(countdownActor, (state) => + state.matches("disabled"), + ); + const [, send] = useActor(countdownActor); + + const disableCounter = () => send({ type: CountdownEventTypes.DISABLE }); + const enableCounter = () => send({ type: CountdownEventTypes.ENABLE }); + const forceRefresh = () => send({ type: CountdownEventTypes.FORCE_FINISH }); + const updateDuration = ( + duration: number, + countdownType = COUNT_DOWN_TYPE.INFINITE, + ) => + send({ + type: CountdownEventTypes.UPDATE_DURATION, + duration, + countdownType, + }); + + return ( + <> + { + updateDuration(5); + }, + disabled: isDisabled, + }, + { + label: "Refresh every 10s", + handler: () => { + updateDuration(10); + }, + disabled: isDisabled, + }, + { + label: "Refresh every 15s", + handler: () => { + updateDuration(15); + }, + disabled: isDisabled, + }, + { + label: "Refresh every 30s", + handler: () => { + updateDuration(30); + }, + disabled: isDisabled, + }, + { + label: "Refresh every 60s", + handler: () => { + updateDuration(60); + }, + disabled: isDisabled, + }, + { + label: `${isDisabled ? "Enable" : "Disable"} Auto Refresh`, + handler: isDisabled ? enableCounter : disableCounter, + }, + ]} + buttonProps={buttonProps} + > + Refresh {duration - elapsed} + + + + ); +}; +interface MaybeAutoRefreshProps { + buttonProps: MuiButtonProps; + countdownActor: ActorRef; + refetch: () => void; + execution: { + status: WorkflowExecutionStatus; + }; +} +const MaybeAutoRefresh: FunctionComponent = ({ + buttonProps, + countdownActor, + refetch, + execution, +}) => { + const [isAnimating, setIsAnimating] = useState(false); + + const handleRefetch = () => { + setIsAnimating(true); + refetch(); + }; + return isNil(countdownActor) ? ( + + ) : ( + + ); +}; + +export default MaybeAutoRefresh; diff --git a/ui-next/src/components/Banner.jsx b/ui-next/src/components/Banner.jsx new file mode 100644 index 0000000000..705ecf2ae6 --- /dev/null +++ b/ui-next/src/components/Banner.jsx @@ -0,0 +1,20 @@ +import { Paper } from "@mui/material"; + +export default function Banner({ children, ...rest }) { + return ( + + {children} + + ); +} diff --git a/ui-next/src/components/ButtonGroup.jsx b/ui-next/src/components/ButtonGroup.jsx new file mode 100644 index 0000000000..1b1aecdcb3 --- /dev/null +++ b/ui-next/src/components/ButtonGroup.jsx @@ -0,0 +1,20 @@ +import { FormControl, InputLabel } from "@mui/material"; +import Button from "./MuiButton"; +import MuiButtonGroup from "./MuiButtonGroup"; + +const ButtonGroup = ({ options, label, style, classes, ...props }) => { + return ( + + {label && {label}} + + {options.map((option, idx) => ( + + ))} + + + ); +}; + +export default ButtonGroup; diff --git a/ui-next/src/components/ButtonTooltip.tsx b/ui-next/src/components/ButtonTooltip.tsx new file mode 100644 index 0000000000..8b41e3b23f --- /dev/null +++ b/ui-next/src/components/ButtonTooltip.tsx @@ -0,0 +1,66 @@ +import { + Fragment, + FunctionComponent, + ReactElement, + ReactNode, + useMemo, +} from "react"; +import { IconButton, IconButtonProps, Tooltip } from "@mui/material"; +import Button, { MuiButtonProps } from "components/MuiButton"; + +export interface ButtonTooltipProps extends MuiButtonProps { + tooltip: NonNullable; + variant?: "contained" | "text" | "outlined"; + disabled?: boolean; + onClick: () => void; + "data-testid"?: string; + displayChildren?: boolean; +} + +export const ButtonTooltip: FunctionComponent = ({ + tooltip, + disabled = false, + onClick, + children, + variant = "contained", + displayChildren = true, + ...otherButtonProps +}) => { + const Container = useMemo( + () => + ({ children }: { children: ReactElement }) => + !tooltip && children != null ? ( + {children} + ) : ( + + {children} + + ), + [tooltip], + ); + return ( + + {displayChildren ? ( + + ) : ( + theme.palette.primary.main }} + {...(otherButtonProps as IconButtonProps)} + > + {otherButtonProps.startIcon} + + )} + + ); +}; diff --git a/ui-next/src/components/CenteredSpinner.tsx b/ui-next/src/components/CenteredSpinner.tsx new file mode 100644 index 0000000000..8c0a09ea4a --- /dev/null +++ b/ui-next/src/components/CenteredSpinner.tsx @@ -0,0 +1,28 @@ +import { Box, CircularProgress, Typography } from "@mui/material"; + +interface CenteredSpinnerProps { + message?: string; +} + +const CenteredSpinner = ({ message }: CenteredSpinnerProps) => ( + + + {message && ( + + {message} + + )} + +); + +export default CenteredSpinner; diff --git a/ui-next/src/components/ClipboardCopy.tsx b/ui-next/src/components/ClipboardCopy.tsx new file mode 100644 index 0000000000..a262ad9c5e --- /dev/null +++ b/ui-next/src/components/ClipboardCopy.tsx @@ -0,0 +1,108 @@ +import { CSSProperties, ReactNode, useState } from "react"; +import { Copy } from "@phosphor-icons/react"; +import { Box, Snackbar, SxProps } from "@mui/material"; +import MuiAlert from "components/MuiAlert"; +import { black } from "theme/tokens/colors"; +import { logger } from "utils"; +import IconButton from "components/MuiIconButton"; + +export interface ClipboardCopyProps { + children?: ReactNode; + value: string; + buttonId?: string; + sx?: SxProps; + linkStyle?: CSSProperties; + iconPlacement?: "start" | "end"; +} + +export default function ClipboardCopy({ + children, + value, + buttonId = "", + sx, + linkStyle, + iconPlacement = "end", +}: ClipboardCopyProps) { + const [isToastOpen, setIsToastOpen] = useState(false); + + function copyContent() { + setIsToastOpen(true); + navigator.clipboard.writeText(value).catch((e) => { + logger.error("Unable to copy to clipboard!", e); + }); + } + + return ( + <> + + + {children} + + + + + + + + setIsToastOpen(false)} + id="copied-to-clipboard-popup" + > + setIsToastOpen(false)} + > + Copied to Clipboard + + + + ); +} diff --git a/ui-next/src/components/CodeBlockInput.tsx b/ui-next/src/components/CodeBlockInput.tsx new file mode 100644 index 0000000000..8d55b6a036 --- /dev/null +++ b/ui-next/src/components/CodeBlockInput.tsx @@ -0,0 +1,151 @@ +import Editor, { EditorProps } from "@monaco-editor/react"; +import { Box, BoxProps, InputLabel, Tooltip } from "@mui/material"; +import { Theme } from "@mui/material/styles"; +import { SxProps } from "@mui/system"; +import { + CSSProperties, + FunctionComponent, + ReactNode, + useCallback, + useContext, + useRef, +} from "react"; +import { ColorModeContext } from "theme/material/ColorModeContext"; +import { inputLabelIdleStyles } from "theme/material/components/formControls"; +import { SMALL_EDITOR_DEFAULT_OPTIONS } from "utils/constants"; +import Text from "./Text"; + +type CodeBlockInputProps = { + label?: ReactNode; + language?: string; + onChange?: (value: string) => void; + value?: string; + containerProps?: BoxProps; + error?: boolean; + height?: number | "auto"; + minHeight?: number; + autoformat?: boolean; + labelStyle?: SxProps; + languageLabel?: string; + containerStyles?: CSSProperties; +} & Partial>; + +const MIN_HEIGHT = 120; + +const CodeBlockInput: FunctionComponent = ({ + label = "Code", + language = "json", + onChange = () => null, + value = "", + containerProps = {}, + error = false, + minHeight, + autoformat = true, + labelStyle, + languageLabel, + containerStyles = {}, + ...restOfProps +}) => { + const { mode } = useContext(ColorModeContext); + const editorRef = useRef(null) as any; + + const handleEditorDidMount = useCallback( + (editor: any) => { + editorRef.current = editor; + if (autoformat) { + editor.onDidBlurEditorWidget(() => { + editor.getAction("editor.action.formatDocument").run(); + }); + } + }, + [editorRef, autoformat], + ); + + const onEditorChange = useCallback(() => { + const editorValue = editorRef?.current?.getValue(); + onChange(editorValue); + }, [onChange]); + + const minimumHeight = minHeight || MIN_HEIGHT; + + return ( + + {label && ( + + {typeof label === "string" && label.length > 30 ? ( + + {label} + + ) : ( + + } + > + {label} + + )} + + + {languageLabel + ? languageLabel.toUpperCase() + : language.toUpperCase()} + + + )} + section": { + resize: "vertical", + overflow: "auto", + minHeight: minimumHeight, + }, + ...containerStyles, + }} + > + + + + ); +}; + +export default CodeBlockInput; diff --git a/ui-next/src/components/ConfirmChoiceDialog.tsx b/ui-next/src/components/ConfirmChoiceDialog.tsx new file mode 100644 index 0000000000..5f3da127ba --- /dev/null +++ b/ui-next/src/components/ConfirmChoiceDialog.tsx @@ -0,0 +1,135 @@ +import { + Box, + Dialog, + DialogActions, + DialogContent, + DialogTitle, +} from "@mui/material"; +import ConductorInput from "components/v1/ConductorInput"; +import SaveIcon from "components/v1/icons/SaveIcon"; +import XCloseIcon from "components/v1/icons/XCloseIcon"; +import { ReactNode, useState } from "react"; +import { Button, Text } from "components/index"; +import ActionButton from "components/ActionButton"; + +const style = { + confirmationMessage: { + opacity: 0.8, + paddingLeft: "10px", + fontSize: "15px", + lineHeight: 1.5, + "& p": { + fontSize: "15px", + fontWeight: "normal", + }, + "& svg": { + fontSize: "15px", + }, + }, +}; + +export default function ConfirmChoiceDialog({ + header = "Confirmation", + message = "Please confirm", + handleConfirmationValue, + isInputConfirmation, + valueToBeDeleted, + cancelBtnLabel, + confirmBtnLabel, + disableBackdropClick, + disableEscapeKeyDown, + hideCancelBtn, + id = "confirm-choice-dialog", + isConfirmLoading = false, +}: { + header?: string; + message?: string | ReactNode; + handleConfirmationValue: (b: boolean) => void; + valueToBeDeleted?: string; + isInputConfirmation?: boolean; + cancelBtnLabel?: string; + confirmBtnLabel?: string; + disableBackdropClick?: boolean; + disableEscapeKeyDown?: boolean; + hideCancelBtn?: boolean; + id?: string; + isConfirmLoading?: boolean; +}) { + const [inputValue, setInputValue] = useState(""); + + const onClose = ( + event: Event, + reason: "backdropClick" | "escapeKeyDown" | "closeButtonClick", + ) => { + if (disableBackdropClick && reason === "backdropClick") { + return false; + } + + handleConfirmationValue(false); + }; + + return ( + + {header} + + + + {message} + + {isInputConfirmation && ( + setInputValue(value)} + fullWidth + color="secondary" + autoFocus + /> + )} + + + + {!hideCancelBtn && ( + + )} + handleConfirmationValue(true)} + disabled={isInputConfirmation && inputValue !== valueToBeDeleted} + color={isInputConfirmation ? "error" : "primary"} + startIcon={confirmBtnLabel ? null : } + progress={isConfirmLoading} + > + {confirmBtnLabel ? confirmBtnLabel : "Confirm"} + + + + ); +} diff --git a/ui-next/src/components/CustomButton.tsx b/ui-next/src/components/CustomButton.tsx new file mode 100644 index 0000000000..0eb2e7e6c0 --- /dev/null +++ b/ui-next/src/components/CustomButton.tsx @@ -0,0 +1,48 @@ +import Button, { MuiButtonProps } from "components/MuiButton"; + +interface CustomButtonProps extends MuiButtonProps { + customVariant?: string; + height?: string; +} + +const CustomButton = ({ + customVariant, + height, + ...props +}: CustomButtonProps) => { + const commonStyle = { + border: `1px solid`, + color: "#060606", + borderRadius: "6px", + ...(height ? { height: height } : {}), + }; + const primaryStyles = { + backgroundColor: "#C8ABFF", + borderColor: "#9157FF", + ":hover": { + backgroundColor: "#C8ABFF", + }, + }; + const secondaryStyles = { + backgroundColor: "transparent", + borderColor: "#161616", + ":hover": { + backgroundColor: "transparent", + }, + }; + const variantStyle = () => { + switch (customVariant) { + case "primary": + return primaryStyles; + case "secondary": + return secondaryStyles; + default: + return primaryStyles; + } + }; + + return + + + + {columns + .map((column, columnIdx) => ( + { + const isChecked = column.omit; + handleChange({ + checked: !isChecked, + columnIdx, + }); + }} + > + + + + )) + .concat( + + Reset to default + , + )} + + + ); +}; diff --git a/ui-next/src/components/DataTable/DataTable.tsx b/ui-next/src/components/DataTable/DataTable.tsx new file mode 100644 index 0000000000..b4ffe60925 --- /dev/null +++ b/ui-next/src/components/DataTable/DataTable.tsx @@ -0,0 +1,568 @@ +import { + Box, + CircularProgress, + Grid, + GridProps, + Theme, + Tooltip, +} from "@mui/material"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import { ArrowDown as SortIcon } from "@phosphor-icons/react"; +import { useMachine, useSelector } from "@xstate/react"; +import { path as _path } from "lodash/fp"; +import _isEmpty from "lodash/isEmpty"; +import _isNil from "lodash/isNil"; +import _isString from "lodash/isString"; +import _noop from "lodash/noop"; +import _omit from "lodash/omit"; +import _stubTrue from "lodash/stubTrue"; +import { + FunctionComponent, + ReactNode, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from "react"; +import RawDataTable, { + TableProps, + TableStyles, +} from "react-data-table-component"; +import { ColorModeContext } from "theme/material/ColorModeContext"; +import { colors } from "theme/tokens/variables"; +import { createTableTitle, logger, useLocalStorage } from "utils"; +import { LOCAL_STORAGE_KEY } from "utils/constants/common"; +import { ColumnsSelector } from "./ColumnSelector"; +import { Filter } from "./Filter"; +import { TagFilter } from "./TagFilter"; +import { + createDefaultFilterObject, + defaultFilterItemsSorter, + formatForColumn, +} from "./helpers"; +import { QuickSearch, QuickSearchProps } from "./QuickSearch"; +import { dataTableMachine } from "./state"; +import { + DataTableEventTypes, + FilterObjectItem, + SerializableColumn, +} from "./state/types"; +import { dataTableStyles } from "./styles"; +import { ColumnCustomType, LegacyColumn, RenderableColumn } from "./types"; + +export const DEFAULT_ROWS_PER_PAGE = 15; + +const headerdefaultbackcolor = "#f8f8f8"; + +export interface DataTableProps extends TableProps { + columns: LegacyColumn[]; //Next step should be enforcing the use of id. in all tables + localStorageKey?: string; + customActions?: ReactNode[]; + defaultShowColumns?: string[]; + showColumnSelector?: boolean; + hideSearch?: boolean; + titleComponent?: ReactNode; + onFilterChange?: (filterObj?: FilterObjectItem) => void; // Dont really understand the undefined here + initialFilterObj?: FilterObjectItem; + quickSearchEnabled?: boolean; + quickSearchPlaceholder?: string; + quickSearchComponent?: FunctionComponent; + createButton?: ReactNode; + sortByDefault?: boolean; + onSearchTermChange?: (searchTerm: string) => void; + description?: ReactNode; + searchTerm?: string; + autoFocus?: boolean; + useGlobalRowsPerPage?: boolean; + searchModalContainerProps?: GridProps; + onRowMouseEnter?: (row: any) => void; + filterByTags?: boolean; +} + +export const DataTable: FunctionComponent = (props) => { + const { + localStorageKey, + columns, + customActions, + data = [], + // options, + defaultShowColumns = [], + pagination = true, + paginationPerPage = 15, + showColumnSelector = true, + paginationServer = false, + hideSearch = false, + title, + titleComponent, + noDataComponent, + onFilterChange, + initialFilterObj, + quickSearchEnabled = false, + quickSearchPlaceholder = "Search", + customStyles, + createButton, + paginationComponent, + sortByDefault = true, + onSearchTermChange = _noop, + description, + quickSearchComponent: QuickSearchComponent = QuickSearch, + searchTerm: inputSearchTerm = "", + autoFocus = true, + onChangeRowsPerPage, + onColumnOrderChange, + useGlobalRowsPerPage = true, + searchModalContainerProps, + onRowMouseEnter, + filterByTags = false, + ...rest + } = props; + const { mode } = useContext(ColorModeContext); + const [persistRowsPerPage, setPersistRowsPerPage] = useLocalStorage( + LOCAL_STORAGE_KEY.ROWS_PER_PAGE, + paginationPerPage, + ); + + // Tag filter state + const [selectedTags, setSelectedTags] = useState([]); + + const handleChangeRowsPerPage = ( + rowsPerPage: number, + currentPage: number, + ) => { + if (useGlobalRowsPerPage) { + setPersistRowsPerPage(rowsPerPage); + } + + if (onChangeRowsPerPage) { + onChangeRowsPerPage(rowsPerPage, currentPage); + } + }; + + // Checking responsive width + const isValidWidth = useMediaQuery((theme: Theme) => + theme.breakpoints.down("sm"), + ); + + // Prepare column renderer. + const renderedColumns = columns.map((column): RenderableColumn => { + const { + id: _id, + name: _name, + wrap = true, + sortable = true, + selector: customSelector, + ...rest + } = column; + const format = formatForColumn(column as LegacyColumn); + return { + id: column.id, + selector: customSelector || ((row: any) => row[column.name as string]), + name: column.label, + sortable: sortable, + wrap: wrap, + // type, + // label, + omit: _isEmpty(defaultShowColumns) + ? false + : !defaultShowColumns.includes(column.id), + reorder: true, + format, + ...rest, + }; + }); + + const [, send, actor] = useMachine(dataTableMachine, { + ...(process.env.NODE_ENV === "development" ? { devTools: true } : {}), + context: { + columnOrderAndVisibility: renderedColumns, // default to column renderer, TODO defaultShowColumns may hold the order + localStorageKey, + filterObj: initialFilterObj || createDefaultFilterObject(renderedColumns), + searchTerm: "", + }, + }); + + // Will hold the order and visibility + const columnOrderVisibility = useSelector( + actor, + (state) => state.context.columnOrderAndVisibility, + ); + + const filterObj = useSelector(actor, (state) => state.context.filterObj); + + const searchTerm = useSelector(actor, (state) => state.context.searchTerm); + + // converts rendered columns to a map, extracting selector that can hold closures. + const renderedColumnsMap = Object.fromEntries( + renderedColumns.map((c) => [c.id, _omit(c, ["omit"])]), + ); + + // Using order and selector construct the resultant. + const orderedColumns = columnOrderVisibility.map((col) => ({ + ...col, + ...renderedColumnsMap[col.id], + })); + + const columnsWithTooltips = orderedColumns.map((column) => { + if (column.tooltip) { + return { + ...column, + name: ( + + {column.label} + + ), + }; + } + return column; + }); + + const handleColumnOrderChange = ( + cols: LegacyColumn[], + viewCols: SerializableColumn[], + ) => { + const viewColumnsNameMapI = Object.fromEntries( + viewCols.map((c) => [c.id, c]), + ); + const columnResult = cols.map(({ id }) => viewColumnsNameMapI[id]); + send({ + type: DataTableEventTypes.SET_ORDER_AND_VISIBILITY, + data: columnResult, + }); + + if (onColumnOrderChange) { + onColumnOrderChange(columnResult); + } + }; + + const handleSearchTermChange = useCallback( + (searchTerm: string) => { + send({ + type: DataTableEventTypes.SET_SEARCH_TERM, + searchTerm, + }); + onSearchTermChange(searchTerm); + }, + [onSearchTermChange, send], + ); + + useEffect(() => { + handleSearchTermChange(inputSearchTerm); + }, [handleSearchTermChange, inputSearchTerm]); + + const handleFilterObjectChange = (filterObj: FilterObjectItem) => { + send({ + type: DataTableEventTypes.SET_FILTER_OBJ, + filterObj, + }); + + // This makes no sense + if (onFilterChange) { + if (!_isEmpty(filterObj.substring)) { + onFilterChange(filterObj); + } else { + onFilterChange(undefined); // This makes no sense to me. + } + } + }; + + const searchTermMaybeFilterFn = useMemo(() => { + if (!quickSearchEnabled) return _stubTrue; // If quick search not enabled return true. + + const searchableColumns = (columns as LegacyColumn[]).reduce( + (result: string[], col: LegacyColumn): string[] => { + if (col.searchable !== false) { + return result.concat(col.name as string); + } + + return result; + }, + [], + ); + + return (row: any) => { + // Don't need to filter cell that has null or undefined value + const rowSearchableColumns = searchableColumns.filter( + (key) => !_isNil(_path(key, row)), + ); + + return rowSearchableColumns.reduce((prev, curr) => { + const fCol = columns.find((column) => column.name === curr); + const rowVal = _path(curr, row); + + const searchableFuncRes = + fCol?.searchableFunc != null + ? fCol + .searchableFunc(rowVal) + .toLowerCase() + .includes(searchTerm?.toLowerCase()) + : rowVal + .toString() + .toLowerCase() + .includes(searchTerm?.toLowerCase()); + + return searchableFuncRes || prev; + }, false); + }; + }, [columns, quickSearchEnabled, searchTerm]); + + // Tag filter function + const tagFilterFn = useCallback( + (row: any) => { + if (selectedTags?.length === 0) return true; + + if (!row?.tags || !Array.isArray(row?.tags)) return false; + + return selectedTags.some((selectedTag) => { + return row?.tags?.some((tag: any) => { + if (tag && tag.key && tag.value) { + const tagString = `${tag?.key}:${tag?.value}`; + return tagString === selectedTag; + } + return false; + }); + }); + }, + [selectedTags], + ); + + const filteredItems = useMemo(() => { + let filtered = data; + + // Apply search term filter + filtered = filtered.filter(searchTermMaybeFilterFn); + + // Apply tag filter if enabled + if (filterByTags) { + filtered = filtered.filter(tagFilterFn); + } + + // Apply column filter if present + if (filterObj !== undefined) { + const column = renderedColumns.find( + (col) => col.id === filterObj.columnName, + ) as RenderableColumn; // This will search on all columns regardless of the column being omitted + if (filterObj.substring && filterObj.columnName) { + try { + const regexp = new RegExp(filterObj.substring, "i"); + const filterObjFilterFn = (row: any, rowIdx: number) => { + let target; + if ( + !_isNil(column?.type) && + (column.type === ColumnCustomType.JSON || + column.type === ColumnCustomType.DATE || + column.searchable === "calculated") + ) { + target = column.format(row, rowIdx); + + if (!_isString(target)) { + target = JSON.stringify(target); + } + } else { + target = column?.selector(row, rowIdx); + } + + // Convert non-string values (including booleans) to strings for regex matching + if (!_isString(target)) { + target = String(target); + } + + return regexp.test(target); + }; + + filtered = filtered.filter(filterObjFilterFn); + } catch (e) { + // Bad or incomplete Regexp + logger.error(e); + return []; + } + } + } + + return filtered; + }, [ + data, + filterObj, + searchTermMaybeFilterFn, + renderedColumns, + filterByTags, + tagFilterFn, + ]); + + return ( + <> + {quickSearchEnabled && ( + + )} + + } + onRowMouseEnter={onRowMouseEnter} + title={ + titleComponent ? ( + titleComponent + ) : ( + + {title + ? title + : createTableTitle({ filteredData: filteredItems, data })} + + ) + } + columns={columnsWithTooltips} + // Sort strategy: + // 1. updateTime 1st (desc) + // 2. If updateTime isn't exist compare with createTime (desc) + // 3. name (asc) + data={ + sortByDefault + ? defaultFilterItemsSorter(filteredItems) + : filteredItems + } + onColumnOrderChange={(col) => + handleColumnOrderChange(col as LegacyColumn[], orderedColumns) + } + pagination={pagination} + paginationServer={paginationServer} + // The persistRowsPerPage variable will be used only if + // the default paginationComponent is used + paginationPerPage={ + paginationComponent || !useGlobalRowsPerPage + ? paginationPerPage + : persistRowsPerPage + } + paginationRowsPerPageOptions={[15, 30, 100]} + noDataComponent={noDataComponent} + paginationComponent={paginationComponent} + progressComponent={ +
    + +
    + } + actions={ + + {customActions && + customActions.length > 0 && + customActions.map((component, index) => ( + + {component} + + ))} + + {!paginationServer && !quickSearchEnabled && !hideSearch && ( + + + + )} + + {filterByTags && ( + + + + )} + + {showColumnSelector && ( + + + + )} + + } + onChangeRowsPerPage={handleChangeRowsPerPage} + {...rest} + /> +
    + + ); +}; +export default DataTable; diff --git a/ui-next/src/components/DataTable/Filter.tsx b/ui-next/src/components/DataTable/Filter.tsx new file mode 100644 index 0000000000..df93dd04e9 --- /dev/null +++ b/ui-next/src/components/DataTable/Filter.tsx @@ -0,0 +1,104 @@ +import { Box, Button, MenuItem, Popover, Tooltip } from "@mui/material"; +import { MagnifyingGlass as SearchIcon } from "@phosphor-icons/react"; +import Input from "components/Input"; +import Select from "components/Select"; +import _get from "lodash/get"; +import _isNil from "lodash/isNil"; +import { FunctionComponent, useState } from "react"; +import { getColumnId, getColumnLabel, getColumnLabelById } from "./helpers"; +import { FilterObjectItem } from "./state"; +import { RenderableColumn } from "./types"; + +export interface FilterProps { + columns: RenderableColumn[]; + filterObj?: FilterObjectItem; + setFilterObj: (filterObject: FilterObjectItem) => void; +} + +export const Filter: FunctionComponent = ({ + columns, + filterObj, + setFilterObj, +}) => { + const [anchorEl, setAnchorEl] = useState(null); + + const handleClick = (event: any) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const handleValueChange = (v: string) => { + setFilterObj({ + columnName: filterObj!.columnName, + substring: v, + }); + }; + + const handleColumnChange = (c: string) => { + setFilterObj({ + columnName: c, + substring: "", + }); + }; + + return ( + <> + + + + + + + + + + + ); +}; diff --git a/ui-next/src/components/DataTable/QuickSearch.tsx b/ui-next/src/components/DataTable/QuickSearch.tsx new file mode 100644 index 0000000000..46ceb9ae70 --- /dev/null +++ b/ui-next/src/components/DataTable/QuickSearch.tsx @@ -0,0 +1,82 @@ +import { Box, Grid, GridProps } from "@mui/material"; +import { FunctionComponent, ReactNode } from "react"; + +import ConductorInput from "components/v1/ConductorInput"; + +export interface QuickSearchProps { + autoFocusValue: boolean; + createButton?: ReactNode; + description?: ReactNode; + onChange: (val: string) => void; + quickSearchLabel?: ReactNode; + quickSearchPlaceholder: string; + searchTerm: string; + searchModalContainerProps?: GridProps; +} + +export const QuickSearch: FunctionComponent = ({ + autoFocusValue, + createButton, + description, + quickSearchLabel = "Quick search", + onChange, + quickSearchPlaceholder, + searchTerm, + searchModalContainerProps, +}) => { + return ( + + + + + + + {createButton && ( + + {createButton} + + )} + + {description && ( + + {description} + + )} + + + ); +}; diff --git a/ui-next/src/components/DataTable/TableRefreshButton.tsx b/ui-next/src/components/DataTable/TableRefreshButton.tsx new file mode 100644 index 0000000000..0a8dd67396 --- /dev/null +++ b/ui-next/src/components/DataTable/TableRefreshButton.tsx @@ -0,0 +1,31 @@ +import { Tooltip } from "@mui/material"; +import { Button } from "components"; +import { colors } from "theme/tokens/variables"; +import { ArrowClockwise as RefreshIcon } from "@phosphor-icons/react"; + +interface TableRefreshButtonProps { + tooltipTitle: string; + refetch: () => void; +} + +export const TableRefreshButton = ({ + tooltipTitle, + refetch, +}: TableRefreshButtonProps) => { + return ( + + + + ); +}; diff --git a/ui-next/src/components/DataTable/TagFilter.tsx b/ui-next/src/components/DataTable/TagFilter.tsx new file mode 100644 index 0000000000..4e9cd54a09 --- /dev/null +++ b/ui-next/src/components/DataTable/TagFilter.tsx @@ -0,0 +1,388 @@ +import React, { useState, FunctionComponent, useMemo } from "react"; +import { + Popover, + Tooltip, + Box, + Button, + Chip, + TextField, + InputAdornment, + Typography, + Divider, + List, + ListItem, + Checkbox, + FormControlLabel, + Accordion, + AccordionSummary, + AccordionDetails, +} from "@mui/material"; +import { + Tag as TagIcon, + MagnifyingGlass as SearchIcon, + CaretDown as ExpandIcon, +} from "@phosphor-icons/react"; +import { TagDto } from "types/Tag"; + +export interface TagFilterProps { + data: Record[]; + onTagFilterChange: (selectedTags: string[]) => void; + selectedTags: string[]; +} + +export const TagFilter: FunctionComponent = ({ + data, + onTagFilterChange, + selectedTags, +}) => { + const [anchorEl, setAnchorEl] = useState(null); + const [searchTerm, setSearchTerm] = useState(""); + const [groupByKey, setGroupByKey] = useState(true); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + setSearchTerm(""); + }; + + // Extract all unique tags from the data and group them + const { allTags, tagsByKey, tagKeys } = useMemo(() => { + const tagMap = new Map< + string, + { key: string; value: string; fullTag: string } + >(); + + data.forEach((row: Record) => { + if (row?.tags && Array.isArray(row?.tags)) { + row?.tags?.forEach((tag: TagDto) => { + if (tag && tag?.key && tag?.value) { + const fullTag = `${tag?.key}:${tag?.value}`; + tagMap.set(fullTag, { key: tag?.key, value: tag?.value, fullTag }); + } + }); + } + }); + + const allTags = Array.from(tagMap?.values())?.sort((a, b) => + a?.fullTag?.localeCompare(b?.fullTag), + ); + + // Group by key + const tagsByKey = new Map(); + allTags?.forEach((tag) => { + if (!tagsByKey?.has(tag?.key)) { + tagsByKey.set(tag?.key, []); + } + tagsByKey?.get(tag?.key)?.push(tag); + }); + + const tagKeys = Array.from(tagsByKey?.keys())?.sort(); + + return { allTags, tagsByKey, tagKeys }; + }, [data]); + + // Filter tags based on search term + const filteredTags = useMemo(() => { + if (!searchTerm) return allTags || []; + + const lowerSearchTerm = searchTerm.toLowerCase(); + return allTags?.filter( + (tag) => + tag?.key?.toLowerCase()?.includes(lowerSearchTerm) || + tag?.value?.toLowerCase()?.includes(lowerSearchTerm) || + tag?.fullTag?.toLowerCase()?.includes(lowerSearchTerm), + ); + }, [allTags, searchTerm]); + + const handleTagToggle = (tag: string) => { + const newSelectedTags = selectedTags?.includes(tag) + ? selectedTags.filter((t) => t !== tag) + : [...selectedTags, tag]; + onTagFilterChange(newSelectedTags); + }; + + const handleClearAll = () => { + onTagFilterChange([]); + }; + + const handleSelectAllInGroup = (key: string) => { + const groupTags = tagsByKey?.get(key) || []; + const groupTagStrings = groupTags?.map((tag) => tag?.fullTag); + const allGroupSelected = groupTagStrings.every((tag) => + selectedTags.includes(tag), + ); + + if (allGroupSelected) { + // Deselect all in group + const newSelectedTags = selectedTags?.filter( + (tag) => !groupTagStrings.includes(tag), + ); + onTagFilterChange(newSelectedTags); + } else { + // Select all in group + const newSelectedTags = [ + ...new Set([...selectedTags, ...groupTagStrings]), + ]; + onTagFilterChange(newSelectedTags); + } + }; + + const renderTagList = () => { + if (allTags?.length === 0) { + return ( + + No tags available + + ); + } + + if (groupByKey && !searchTerm) { + // Grouped view + return ( + + {tagKeys.map((key) => { + const groupTags = tagsByKey?.get(key) || []; + const groupTagStrings = groupTags?.map((tag) => tag?.fullTag); + const selectedInGroup = groupTagStrings?.filter((tag) => + selectedTags?.includes(tag), + ); + const allGroupSelected = + groupTagStrings?.length === selectedInGroup?.length; + const someGroupSelected = selectedInGroup?.length > 0; + + return ( + + }> + handleSelectAllInGroup(key)} + onClick={(e) => e.stopPropagation()} + size="small" + /> + } + label={ + + {key} ({groupTags?.length}) + + } + onClick={(e) => e.stopPropagation()} + /> + + + + {groupTags?.map((tag) => ( + + handleTagToggle(tag?.fullTag)} + size="small" + /> + } + label={ + + {tag?.value} + + } + /> + + ))} + + + + ); + })} + + ); + } else { + // Flat list view (when searching or groupByKey is false) + return ( + + {filteredTags?.map((tag) => ( + + handleTagToggle(tag?.fullTag)} + size="small" + /> + } + label={ + + {tag?.fullTag} + + } + /> + + ))} + {filteredTags?.length === 0 && searchTerm && ( + + + No tags found matching "{searchTerm}" + + + )} + + ); + } + }; + + return ( + <> + + + + + + {/* Header */} + + + Filter by Tags + + {selectedTags?.length > 0 && ( + + )} + + + {/* Search */} + setSearchTerm(e.target.value)} + InputProps={{ + startAdornment: ( + + + + ), + }} + sx={{ mb: 2 }} + /> + + {/* Group by key toggle (only show when not searching) */} + {!searchTerm && allTags?.length > 10 && ( + setGroupByKey(e.target.checked)} + size="small" + /> + } + label="Group by key" + sx={{ mb: 1 }} + /> + )} + + + + {/* Tag list */} + {renderTagList()} + + {/* Selected tags summary */} + {selectedTags?.length > 0 && ( + <> + + + + Selected ({selectedTags?.length}): + + + {selectedTags?.slice(0, 5)?.map((tag) => ( + handleTagToggle(tag)} + color="primary" + variant="filled" + /> + ))} + {selectedTags?.length > 5 && ( + + )} + + + + )} + + + + ); +}; diff --git a/ui-next/src/components/DataTable/helpers.test.ts b/ui-next/src/components/DataTable/helpers.test.ts new file mode 100644 index 0000000000..43763da1d5 --- /dev/null +++ b/ui-next/src/components/DataTable/helpers.test.ts @@ -0,0 +1,180 @@ +import { dynamicSort } from "./helpers"; + +const cases = [ + { + name: "A greater than B", + objA: { + target: { + type: "INTEGRATION_PROVIDER", + id: "My Test integration", + }, + access: ["UPDATE", "READ", "EXECUTE", "CREATE"], + tag: "test:test", + }, + objB: { + target: { + type: "APPLICATION", + id: "app:8ee1b276-28a1-443c-b5a4-43892f87e222", + }, + access: ["READ", "CREATE"], + tag: "auto:test", + }, + propertyPath: "target.type", + expected: 1, + }, + { + name: "A lesser than B", + objA: { + target: { + type: "INTEGRATION_PROVIDER", + id: "0_My Test integration", + }, + access: ["UPDATE", "READ", "EXECUTE", "CREATE"], + tag: "test:test", + }, + objB: { + target: { + type: "APPLICATION", + id: "app:8ee1b276-28a1-443c-b5a4-43892f87e222", + }, + access: ["READ", "CREATE"], + tag: "auto:test", + }, + propertyPath: "target.id", + expected: -1, + }, + { + name: "A equal B", + objA: { + target: { + type: "INTEGRATION_PROVIDER", + id: "0_My Test integration", + number: 9, + }, + access: ["UPDATE", "READ", "EXECUTE", "CREATE"], + tag: "test:test", + }, + objB: { + target: { + type: "APPLICATION", + id: "app:8ee1b276-28a1-443c-b5a4-43892f87e222", + number: 9, + }, + access: ["READ", "CREATE"], + tag: "auto:test", + }, + propertyPath: "target.number", + expected: 0, + }, + { + name: "A and B have undefined value", + objA: { + target: { + type: "INTEGRATION_PROVIDER", + id: "0_My Test integration", + number: 9, + }, + access: ["UPDATE", "READ", "EXECUTE", "CREATE"], + tag: "test:test", + }, + objB: { + target: { + type: "APPLICATION", + id: "app:8ee1b276-28a1-443c-b5a4-43892f87e222", + number: 9, + }, + access: ["READ", "CREATE"], + tag: "auto:test", + }, + propertyPath: "target.someProp.a.b.c", + expected: 0, + }, + { + name: "A and B are arrays and have different length", + objA: { + target: { + type: "INTEGRATION_PROVIDER", + id: "0_My Test integration", + number: 9, + }, + access: ["UPDATE", "READ", "EXECUTE", "CREATE"], + tag: "test:test", + }, + objB: { + target: { + type: "APPLICATION", + id: "app:8ee1b276-28a1-443c-b5a4-43892f87e222", + number: 9, + }, + access: ["READ", "CREATE"], + tag: "auto:test", + }, + propertyPath: "access", + expected: 1, + }, + { + name: "A and B are different objects", + objA: { + target: { + type: "INTEGRATION_PROVIDER", + id: "0_My Test integration", + number: 9, + }, + access: ["UPDATE", "READ", "EXECUTE", "CREATE"], + tag: "test:test", + }, + objB: { + target: { + type: "APPLICATION", + id: "app:8ee1b276-28a1-443c-b5a4-43892f87e222", + number: 9, + }, + access: ["READ", "CREATE"], + tag: "auto:test", + }, + propertyPath: "target", + expected: 1, + }, + { + name: "A and B are equal objects", + objA: { + target: { + type: "INTEGRATION_PROVIDER", + id: "0_My Test integration", + number: 9, + }, + access: ["UPDATE", "READ", "EXECUTE", "CREATE"], + tag: "test:test", + equal: { + a: 1, + b: 2, + }, + }, + objB: { + target: { + type: "APPLICATION", + id: "app:8ee1b276-28a1-443c-b5a4-43892f87e222", + number: 9, + }, + access: ["READ", "CREATE"], + tag: "auto:test", + equal: { + a: 1, + b: 2, + }, + }, + propertyPath: "equal", + expected: 0, + }, +]; + +describe("Compare 2 objects for sorting", () => { + test.each(cases)( + "testing '$name', returns $expected", + ({ objA, objB, propertyPath, expected }) => { + const result = dynamicSort({ objA, objB, propertyPath }); + + expect(result).toEqual(expected); + }, + ); +}); diff --git a/ui-next/src/components/DataTable/helpers.ts b/ui-next/src/components/DataTable/helpers.ts new file mode 100644 index 0000000000..9f5b185f40 --- /dev/null +++ b/ui-next/src/components/DataTable/helpers.ts @@ -0,0 +1,147 @@ +import fastDeepEqual from "fast-deep-equal"; +import _get from "lodash/get"; +import { TableColumn } from "react-data-table-component"; +import { timestampRenderer } from "utils/index"; +import { FilterObjectItem } from "./state/types"; +import { + ColumnCustomType, + Format, + LegacyColumn, + RenderableColumn, +} from "./types"; + +type ColumnWithLabel = TableColumn & { label?: string }; + +export const getColumnLabelById = ( + columnId: string, + columns: ColumnWithLabel[], +) => { + const col = columns.find( + (c: ColumnWithLabel) => c.id === columnId || c.name === columnId, + ); + return col?.label || col?.name; +}; +export const getColumnLabel = (col: ColumnWithLabel) => + (col?.label || col.name!) as string; + +export const getColumnId = (col: TableColumn): string => { + return col.id as string; +}; + +const compareString = (preString: string, curString: string) => + preString.toLowerCase().localeCompare(curString.toLowerCase()); + +export const defaultFilterItemsSorter = (filteredItems: any[]) => + filteredItems?.sort((preValue, curValue) => { + if (preValue.updateTime) { + if (curValue.updateTime) { + return curValue.updateTime - preValue.updateTime; + } + + if (curValue.createTime) { + return curValue.createTime - preValue.updateTime; + } + } else if (preValue.createTime) { + if (curValue.updateTime) { + return curValue.updateTime - preValue.createTime; + } + + if (curValue.createTime) { + return curValue.createTime - preValue.createTime; + } + } else if (preValue.size && curValue.size) { + return curValue.size - preValue.size; + } + // Compare name + else if (preValue.name && curValue.name) { + return compareString(preValue.name, curValue.name); + } + // Compare id (for group) + else if (preValue.id && curValue.id) { + return compareString(preValue.id, curValue.id); + } + + return 0; + }); + +export const formatForColumn = (column: LegacyColumn): Format => { + if (column?.type === ColumnCustomType.DATE) { + return (row: any) => timestampRenderer(_get(row, column.name as string)); + } else if (column?.type === ColumnCustomType.JSON) { + return (row: any) => JSON.stringify(_get(row, column.name as string)); + } + + if (column?.renderer) { + return (row: any) => + column.renderer!(_get(row, column.name as string), row); + } + return (row: any) => _get(row, column.name as string); +}; + +export const createDefaultFilterObject = ( + renderedColumns: RenderableColumn[], +): FilterObjectItem | undefined => { + const maybeColumnName = renderedColumns.find( + (col: LegacyColumn) => col.searchable !== false, + )?.id; + if (maybeColumnName) { + return { + columnName: maybeColumnName, + substring: "", + }; + } +}; + +export const getNestedValue = (obj: T, path: string): unknown => { + return path.split(".").reduce((acc: any, part) => acc && acc[part], obj); +}; + +export const dynamicSort = ({ + objA, + objB, + propertyPath, +}: { + objA: T; + objB: T; + propertyPath: string; +}): number => { + const valueA = getNestedValue(objA, propertyPath); + const valueB = getNestedValue(objB, propertyPath); + + if ( + valueA === undefined || + valueA === null || + valueB === undefined || + valueB === null + ) { + if (valueA === valueB) { + return 0; + } + + return valueA === undefined || valueA === null ? 1 : -1; + } + + if (typeof valueA === "string" && typeof valueB === "string") { + return valueA.toLowerCase().localeCompare(valueB.toLowerCase()); + } + + if (typeof valueA === "number" && typeof valueB === "number") { + return valueA - valueB; + } + + if (typeof valueA === "boolean" && typeof valueB === "boolean") { + return valueA === valueB ? 0 : valueA ? -1 : 1; + } + + if (Array.isArray(valueA) && Array.isArray(valueB)) { + return Math.sign(valueA.length - valueB.length); + } + + if (typeof valueA === "object" && typeof valueB === "object") { + const result = fastDeepEqual(valueA, valueB); + + return result ? 0 : 1; + } + + return valueA.toString().localeCompare(valueB.toString()); +}; diff --git a/ui-next/src/components/DataTable/index.ts b/ui-next/src/components/DataTable/index.ts new file mode 100644 index 0000000000..a74c24a803 --- /dev/null +++ b/ui-next/src/components/DataTable/index.ts @@ -0,0 +1,2 @@ +import DataTable from "./DataTable"; +export { DataTable }; diff --git a/ui-next/src/components/DataTable/state/actions.ts b/ui-next/src/components/DataTable/state/actions.ts new file mode 100644 index 0000000000..94bdba54a9 --- /dev/null +++ b/ui-next/src/components/DataTable/state/actions.ts @@ -0,0 +1,32 @@ +import { assign } from "xstate"; +import { + DataTableMachineContext, + SetFilterObjectEvent, + SetSearchTermEvent, + SetTableDataOrderAndVisibility, +} from "./types"; +// For now we are not managing state for data +/* export const persistData = assign({ */ +/* data: (_context, { data }) => data, */ +/* }); */ + +export const persistOrderAndVisibility = assign< + DataTableMachineContext, + SetTableDataOrderAndVisibility +>({ + columnOrderAndVisibility: (_context, { data }) => data, +}); + +export const persistSearchTerm = assign< + DataTableMachineContext, + SetSearchTermEvent +>({ + searchTerm: (context, { searchTerm }) => searchTerm, +}); + +export const persistFilterObj = assign< + DataTableMachineContext, + SetFilterObjectEvent +>({ + filterObj: (context, { filterObj }) => filterObj, +}); diff --git a/ui-next/src/components/DataTable/state/guards.ts b/ui-next/src/components/DataTable/state/guards.ts new file mode 100644 index 0000000000..8630c977fb --- /dev/null +++ b/ui-next/src/components/DataTable/state/guards.ts @@ -0,0 +1,16 @@ +import { DoneInvokeEvent } from "xstate"; +import { DataTableMachineContext, SerializableColumn } from "./types"; +import { getColumnId } from "../helpers"; + +import _isNil from "lodash/isNil"; + +export const noLocalStorageKey = (context: DataTableMachineContext) => + _isNil(context.localStorageKey); + +export const isLocalStorageContentTrusted = ( + { columnOrderAndVisibility }: DataTableMachineContext, + { data }: DoneInvokeEvent, +) => { + const existingColumns: string[] = columnOrderAndVisibility.map(getColumnId); + return data.every((a) => !_isNil(a) && existingColumns.includes(a?.id)); +}; diff --git a/ui-next/src/components/DataTable/state/index.ts b/ui-next/src/components/DataTable/state/index.ts new file mode 100644 index 0000000000..79fd82215f --- /dev/null +++ b/ui-next/src/components/DataTable/state/index.ts @@ -0,0 +1,2 @@ +export * from "./machine"; +export * from "./types"; diff --git a/ui-next/src/components/DataTable/state/machine.ts b/ui-next/src/components/DataTable/state/machine.ts new file mode 100644 index 0000000000..ffb8ba6893 --- /dev/null +++ b/ui-next/src/components/DataTable/state/machine.ts @@ -0,0 +1,95 @@ +import { createMachine } from "xstate"; +import * as actions from "./actions"; +import * as services from "./services"; +import * as guards from "./guards"; +import { + DataTableMachineContext, + DataTableEventTypes, + DataTableEvents, +} from "./types"; + +export const dataTableMachine = createMachine< + DataTableMachineContext, + DataTableEvents +>( + { + id: "dataTableMachine", + predictableActionArguments: true, + initial: "init", + context: { + columnOrderAndVisibility: [], + localStorageKey: undefined, + searchTerm: "", + }, + on: { + // [DataTableEventTypes.SET_DATA]: { + // actions: ["persistData"], + // }, + [DataTableEventTypes.SET_SEARCH_TERM]: { + actions: ["persistSearchTerm"], + }, + [DataTableEventTypes.SET_FILTER_OBJ]: { + actions: ["persistFilterObj"], + }, + }, + states: { + init: { + always: [ + { + cond: "noLocalStorageKey", + target: "renderedTable", + }, + { target: "takeLocalStorageConfigurations" }, + ], + }, + renderedTable: { + on: { + [DataTableEventTypes.SET_ORDER_AND_VISIBILITY]: { + actions: ["persistOrderAndVisibility"], + }, + }, + }, + renderedTableStorageSupport: { + on: { + [DataTableEventTypes.SET_ORDER_AND_VISIBILITY]: { + actions: ["persistOrderAndVisibility"], + target: "persisOrderToLocalStorage", + }, + }, + }, + useLocalStorageOrderAndVisibility: { + entry: "persistOrderAndVisibility", + always: "renderedTableStorageSupport", + }, + persisOrderToLocalStorage: { + invoke: { + src: "saveOrderAndVisibility", + id: "localStoragePersistOrderAndVisibility", + onDone: { + target: "renderedTableStorageSupport", + }, + }, + }, + takeLocalStorageConfigurations: { + invoke: { + src: "maybePullOrderAndVisibility", + id: "localStoragePull", + onDone: [ + { + cond: "isLocalStorageContentTrusted", + target: "useLocalStorageOrderAndVisibility", + }, + { + target: "renderedTableStorageSupport", + }, + ], + }, + }, + }, + }, + { + actions: actions as any, + services, + guards: guards as any, + }, +); diff --git a/ui-next/src/components/DataTable/state/services.ts b/ui-next/src/components/DataTable/state/services.ts new file mode 100644 index 0000000000..973e5974c2 --- /dev/null +++ b/ui-next/src/components/DataTable/state/services.ts @@ -0,0 +1,39 @@ +import { tryToJson } from "utils/utils"; +import { DataTableMachineContext } from "./types"; +import { logger } from "utils/logger"; + +export const saveOrderAndVisibility = async ( + context: DataTableMachineContext, +) => { + const { localStorageKey, columnOrderAndVisibility } = context; + if (localStorageKey) { + window.localStorage.setItem( + localStorageKey, + JSON.stringify(columnOrderAndVisibility), + ); + + return columnOrderAndVisibility; + } + throw Error("No local storage key has been set"); +}; + +export const maybePullOrderAndVisibility = async ( + context: DataTableMachineContext, +) => { + const { localStorageKey, columnOrderAndVisibility } = context; + if (localStorageKey) { + const savedOrder = window.localStorage.getItem(localStorageKey); + if (savedOrder) { + const parsedSavedOrder = tryToJson(savedOrder); + if (parsedSavedOrder !== undefined) { + return parsedSavedOrder; + } else { + window.localStorage.removeItem(localStorageKey); + logger.log( + "Couldn't parse savedOrder hence removing it from localStorage and returning columnOrderAndVisibility.", + ); + } + } + } + return columnOrderAndVisibility; +}; diff --git a/ui-next/src/components/DataTable/state/types.ts b/ui-next/src/components/DataTable/state/types.ts new file mode 100644 index 0000000000..07545314c2 --- /dev/null +++ b/ui-next/src/components/DataTable/state/types.ts @@ -0,0 +1,56 @@ +import { ColumnCustomType } from "components/DataTable/types"; +import type { ReactNode } from "react"; +export interface SerializableColumn { + omit: boolean; + name: string | ReactNode; + id: string; // We should enforce the use of id, + wrap?: boolean; + label: string; + sortable?: boolean; + type?: ColumnCustomType; + searchable?: string | boolean; + tooltip?: string; +} + +export interface FilterObjectItem { + columnName: string; + substring: string; +} + +export interface DataTableMachineContext { + columnOrderAndVisibility: SerializableColumn[]; + localStorageKey?: string; + searchTerm: string; + filterObj?: FilterObjectItem; +} + +export enum DataTableEventTypes { + SET_DATA = "SET_DATA", + SET_ORDER_AND_VISIBILITY = "SET_ORDER_AND_VISIBILITY", + SET_FILTER_OBJ = "SET_FILTER_OBJ", + SET_SEARCH_TERM = "SET_SEARCH_TERM", +} + +export type SetTableDataEvent = { + type: DataTableEventTypes.SET_DATA; + data: any[]; +}; + +export type SetTableDataOrderAndVisibility = { + type: DataTableEventTypes.SET_ORDER_AND_VISIBILITY; + data: SerializableColumn[]; +}; + +export type SetSearchTermEvent = { + type: DataTableEventTypes.SET_SEARCH_TERM; + searchTerm: string; +}; + +export type SetFilterObjectEvent = { + type: DataTableEventTypes.SET_FILTER_OBJ; + filterObj: FilterObjectItem; +}; + +export type DataTableEvents = + /* | SetTableDataEvent */ + SetTableDataOrderAndVisibility | SetFilterObjectEvent | SetSearchTermEvent; diff --git a/ui-next/src/components/DataTable/styles.ts b/ui-next/src/components/DataTable/styles.ts new file mode 100644 index 0000000000..fcef06f9dd --- /dev/null +++ b/ui-next/src/components/DataTable/styles.ts @@ -0,0 +1,115 @@ +import { createTheme } from "react-data-table-component"; + +createTheme("default", { + background: { + default: "transparent", + text: { + primary: "#777777", + // secondary: "blue", + }, + highlightOnHover: { + text: "#000000", + default: "rgba(128, 128, 128, .3)", + }, + }, +}); + +// createTheme creates a new theme named solarized that overrides the build in dark theme +createTheme("dark", { + header: { + style: { + color: "#aaaaaa", + fontSize: "14px", + }, + }, + headRow: { + style: { + backgroundColor: "rgba(0,0,0,.03)", + width: "100%", + }, + }, + text: { + primary: "#FFFFFF", + secondary: "rgba(255, 255, 255, 0.7)", + disabled: "rgba(0,0,0,.12)", + }, + background: { + default: "#111111", + }, + context: { + background: "var(--primaryDarker)", + text: "#FFFFFF", + }, + divider: { + default: "rgba(81, 81, 81, .5)", + }, + button: { + default: "#FFFFFF", + focus: "rgba(255, 255, 255, .54)", + hover: "rgba(255, 255, 255, .12)", + disabled: "rgba(255, 255, 255, .18)", + }, + selected: { + default: "rgba(0, 0, 0, .7)", + text: "#FFFFFF", + }, + highlightOnHover: { + default: "rgba(128, 128, 128, .2)", + text: "#FFFFFF", + }, + striped: { + default: "rgba(0, 0, 0, .87)", + text: "#FFFFFF", + }, +}); + +export const dataTableStyles = { + // Top title (e.g. "Page 1 of Many") + header: { + style: { + color: "#111111", + fontSize: "14px", + flex: 0, + }, + }, + headRow: { + style: { + backgroundColor: "rgba(0,0,0,.03)", + width: "100%", + }, + }, + subHeader: { + style: { + backgroundColor: "blue", + color: "red", + }, + }, + headCells: { + style: { + display: "flex", + justifyContent: "start", + alignItems: "center", + textTransform: "uppercase", + fontSize: "11px", + fontWeight: 600, + padding: "5px 16px", + }, + }, + cells: { + style: { + padding: "10px 16px", + alignItems: "center", + fontWeight: 300, + }, + }, + contextMenu: { + style: { + borderRadius: "8px 8px 0 0", + }, + }, + pagination: { + style: { + flex: 0, + }, + }, +}; diff --git a/ui-next/src/components/DataTable/types.ts b/ui-next/src/components/DataTable/types.ts new file mode 100644 index 0000000000..ae84eb2838 --- /dev/null +++ b/ui-next/src/components/DataTable/types.ts @@ -0,0 +1,30 @@ +import { ReactNode } from "react"; +import type { Selector, TableColumn } from "react-data-table-component"; + +export type Format = (row: T, rowIndex: number) => ReactNode; + +export type PaginationChangePage = (page: number, totalRows: number) => void; + +export enum ColumnCustomType { + DATE = "date", + JSON = "json", +} + +export interface LegacyColumn extends TableColumn { + id: string; + name: string | ReactNode; + label: string; + type?: ColumnCustomType; + sortable?: boolean; + wrap?: boolean; + searchable?: string | boolean; + renderer?: (value: any, row: any) => ReactNode; + searchableFunc?: (row: any) => string; + tooltip?: string; +} + +export interface RenderableColumn extends LegacyColumn { + format: Format; + selector: Selector; + omit: boolean; +} diff --git a/ui-next/src/components/DateRangePicker.tsx b/ui-next/src/components/DateRangePicker.tsx new file mode 100644 index 0000000000..10860e5694 --- /dev/null +++ b/ui-next/src/components/DateRangePicker.tsx @@ -0,0 +1,85 @@ +import { Box, Grid } from "@mui/material"; +import { DateTimePicker } from "@mui/x-date-pickers/DateTimePicker"; +import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; +import { dateRangePickerStyle } from "shared/styles"; +import { convertToDateObject, DateAdapter, formatDate } from "utils/date"; + +interface DateRangePickerProps { + onFromChange: (val: any) => void; + onToChange: (val: any) => void; + from: Date | string; + to: Date | string; + label: string; + labelFrom?: string; + labelTo?: string; + disabled: boolean; +} + +export default function DateRangePicker({ + onFromChange, + from, + onToChange, + to, + label, + labelFrom, + labelTo, + disabled, +}: DateRangePickerProps) { + const actualLabelFrom = + labelFrom == null ? label && `${label} - from` : labelFrom; + const actualLabelTo = labelTo == null ? label && `${label} - to` : labelTo; + + return ( + + + + + { + onFromChange(formatDate(value, "yyyy-MM-dd'T'HH:mm:ss")); + }} + sx={dateRangePickerStyle.input} + slotProps={{ + actionBar: { + actions: ["clear", "accept"], + }, + }} + /> + + + { + onToChange(formatDate(value, "yyyy-MM-dd'T'HH:mm:ss")); + }} + sx={dateRangePickerStyle.input} + slotProps={{ + actionBar: { + actions: ["clear", "accept"], + }, + }} + /> + + + + + ); +} diff --git a/ui-next/src/components/DiffEditor/DiffEditor.tsx b/ui-next/src/components/DiffEditor/DiffEditor.tsx new file mode 100644 index 0000000000..1a3129659b --- /dev/null +++ b/ui-next/src/components/DiffEditor/DiffEditor.tsx @@ -0,0 +1,18 @@ +import { DiffEditor as MonacoDiffEditor } from "@monaco-editor/react"; +import { type DiffEditorOptions } from "shared/editor"; +import "./diff-editor.css"; + +const defaultOptions: DiffEditorOptions = { + useInlineViewWhenSpaceIsLimited: false, + renderGutterMenu: false, + scrollbar: { + vertical: "visible", + horizontal: "hidden", + }, +}; + +export const DiffEditor = ({ options = {}, ...rest }) => { + return ( + + ); +}; diff --git a/ui-next/src/components/DiffEditor/diff-editor.css b/ui-next/src/components/DiffEditor/diff-editor.css new file mode 100644 index 0000000000..27bf5bc7b7 --- /dev/null +++ b/ui-next/src/components/DiffEditor/diff-editor.css @@ -0,0 +1,3 @@ +.monaco-diff-editor .diffOverview .diffViewport { + background: rgba(100, 100, 100, 0.03); +} diff --git a/ui-next/src/components/DocLink.tsx b/ui-next/src/components/DocLink.tsx new file mode 100644 index 0000000000..a9950cedf7 --- /dev/null +++ b/ui-next/src/components/DocLink.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import MuiTypography from "./MuiTypography"; +import { colors } from "theme/tokens/variables"; +import { openInNewTab } from "utils/helpers"; +import DocsIcon from "./v1/icons/DocsIcon"; + +interface DocLinkProps { + url: string; + label: string; + position?: "relative" | "absolute"; + right?: string; + top?: string; +} + +export const DocLink = ({ + url, + label, + position = "absolute", + right = "20px", + top = "5px", +}: DocLinkProps) => { + return ( + openInNewTab(url)} + > + {label} + + ); +}; diff --git a/ui-next/src/components/Dropdown.tsx b/ui-next/src/components/Dropdown.tsx new file mode 100644 index 0000000000..9a35e013a4 --- /dev/null +++ b/ui-next/src/components/Dropdown.tsx @@ -0,0 +1,145 @@ +import CancelOutlinedIcon from "@mui/icons-material/CancelOutlined"; +import { + Autocomplete, + AutocompleteProps, + CSSObject, + Chip, + FormControl, + TextFieldPropsSizeOverrides, + Theme, +} from "@mui/material"; +import { OverridableStringUnion } from "@mui/types"; +import { ForwardedRef, ReactNode, forwardRef, useEffect, useRef } from "react"; +import { colors } from "theme/tokens/variables"; +import Input, { CustomInputProps } from "./Input"; + +const autocompleteStyle = { + ".MuiAutocomplete-popupIndicator": { + color: (theme: Theme) => + theme.palette.mode === "dark" ? colors.white : colors.black, + }, +}; + +// Define the option types more clearly +type DropdownOption = string | number | { label: string }; + +// Simplified approach that works better with MUI's complex generics +type DropdownProps = Omit< + AutocompleteProps< + DropdownOption, + boolean | undefined, + boolean | undefined, + boolean | undefined + >, + "renderInput" | "onInputChange" | "options" +> & { + onInputChange?: (value: string) => void; + label?: ReactNode; + style?: CSSObject; + error?: boolean; + size?: OverridableStringUnion< + "small" | "medium", + TextFieldPropsSizeOverrides + >; + helperText?: ReactNode; + inputProps?: CustomInputProps; + required?: boolean; + options?: readonly DropdownOption[]; +}; + +const Dropdown = forwardRef( + ( + { + label, + className, + style, + error, + size, + helperText, + inputProps, + required, + autoFocus, + onInputChange, + options, + ...props + }: DropdownProps, + ref: ForwardedRef, + ) => { + const inputRef = useRef(null); + + useEffect(() => { + if (autoFocus && inputRef.current?.focus) { + inputRef.current.focus(); + } + }, [autoFocus]); + + const handleInputChange = (typingValue: string) => { + if (onInputChange) { + onInputChange(typingValue); + } + }; + + const isRequired = + required && + (!props?.multiple || + (Array.isArray(props?.value) && props?.value?.length === 0)); + + const { InputProps: inputPropsInputProps, ...restInputProps } = + inputProps || { InputProps: {} }; + + return ( + + ( + + )} + renderTags={(value, getTagProps) => + (value as DropdownOption[]).map((v, index) => { + const renderableLabel: string = + typeof v === "string" || typeof v === "number" + ? String(v) + : v.label; + const { key, ...otherTagProps } = getTagProps({ index }); + return ( + } + /> + ); + }) + } + options={options || []} + {...props} + /> + + ); + }, +); + +export default Dropdown; diff --git a/ui-next/src/components/DropdownButton.tsx b/ui-next/src/components/DropdownButton.tsx new file mode 100644 index 0000000000..e58a9af8c1 --- /dev/null +++ b/ui-next/src/components/DropdownButton.tsx @@ -0,0 +1,123 @@ +import { Fragment, MouseEvent, ReactNode, useRef, useState } from "react"; +import { CaretDown } from "@phosphor-icons/react"; +import ClickAwayListener from "@mui/material/ClickAwayListener"; +import Grow from "@mui/material/Grow"; +import Popper from "@mui/material/Popper"; +import MenuItem from "@mui/material/MenuItem"; +import MenuList from "@mui/material/MenuList"; +import Paper from "./Paper"; // TODO check where this is used seems like specific +import Button, { MuiButtonProps } from "components/MuiButton"; +import Divider from "@mui/material/Divider"; + +export type DropdownButtonProps = { + buttonProps?: MuiButtonProps; + children?: ReactNode; + options: any[]; + isOpen?: boolean; + onClick?: (e: MouseEvent, open: boolean) => void; + onClickAway?: (e: any) => void; +}; + +export default function DropdownButton({ + children, + options, + buttonProps, + isOpen, + onClick, + onClickAway, +}: DropdownButtonProps) { + const [open, setOpen] = useState(false); + const isOpenMenu = typeof isOpen === "boolean" ? isOpen : open; + const anchorRef = useRef(null); + + const handleToggle = (e: MouseEvent) => { + setOpen((prevOpen) => !prevOpen); + + if (onClick) { + onClick(e, !isOpenMenu); + } + }; + + const handleClose = (event: any) => { + if (anchorRef.current && anchorRef.current.contains(event.target)) { + return; + } + + setOpen(false); + + if (onClickAway) { + onClickAway(event); + } + }; + + return ( + + + + + {({ TransitionProps, placement }) => ( + + + + + {options.map( + ({ label, handler, disabled, isDivider }, index) => { + const itemId = `${label}-${index}`; + + return isDivider ? ( + + ) : ( + { + handler(event, index); + setOpen(false); + }} + disabled={disabled} + > + {label} + + ); + }, + )} + + + + + )} + + + ); +} diff --git a/ui-next/src/components/EditInPlace.tsx b/ui-next/src/components/EditInPlace.tsx new file mode 100644 index 0000000000..57fd079634 --- /dev/null +++ b/ui-next/src/components/EditInPlace.tsx @@ -0,0 +1,78 @@ +import { FunctionComponent } from "react"; +import { Box, BoxProps } from "@mui/material"; +import { PencilSimple } from "@phosphor-icons/react"; + +export interface EditInPlaceProps extends BoxProps { + text: string; + type: string; + placeholder: string; + childRef: any; + disabled?: boolean; + isEditing: boolean; + setEditing: (editing: boolean) => void; + toggleMetaBarEditMode?: (isMetaBarEditing: boolean) => void; +} + +const EditInPlace: FunctionComponent = ({ + text, + type, + placeholder, + children, + childRef: _childRef, + disabled, + toggleMetaBarEditMode: _toggleMetaBarEditMode, + isEditing, + setEditing, + ...props +}) => { + const toggleEditMode = (isOpen: boolean) => { + setEditing(isOpen); + }; + + const handleKeyDown = (event: any, type: any) => { + const { key } = event; + const keys = ["Escape", "Tab"]; + const enterKey = "Enter"; + const allKeys = [...keys, enterKey]; + if ( + (type === "textarea" && keys.indexOf(key) > -1) || + (type !== "textarea" && allKeys.indexOf(key) > -1) + ) { + toggleEditMode(false); + } + }; + + return ( + + {isEditing ? ( + { + toggleEditMode(false); + }} + onKeyDown={(e) => handleKeyDown(e, type)} + {...props} + > + {children} + + ) : ( + { + return disabled ? null : toggleEditMode(true); + }} + > + + {text || placeholder || "Click here to edit..."} + {!disabled && ( + + )} + + + )} + + ); +}; + +export default EditInPlace; diff --git a/ui-next/src/components/EmptyPageIntro.tsx b/ui-next/src/components/EmptyPageIntro.tsx new file mode 100644 index 0000000000..378359eef5 --- /dev/null +++ b/ui-next/src/components/EmptyPageIntro.tsx @@ -0,0 +1,218 @@ +import { Box, Button, Link, colors, Stack } from "@mui/material"; +import React from "react"; +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; +import ReactPlayer from "react-player"; +import TagChip from "./TagChip"; +import { logrocketTrackIfEnabled } from "utils/logrocket"; + +export interface EmptyPageIntroProps { + id?: string; + image?: string; + videoUrl?: string; + title: React.ReactNode; + message: string; + variant?: "featureDisabled" | "default"; + primaryAction?: { + text: string; + onClick: () => void; + disabled?: boolean; + startIcon?: React.ReactNode; + }; + secondaryAction?: { + text: string; + onClick: () => void; + disabled?: boolean; + startIcon?: React.ReactNode; + }; + footer?: string; +} + +const EmptyPageIntro = ({ + id, + image, + videoUrl, + title, + message, + primaryAction, + secondaryAction, + footer, + variant = "default", +}: EmptyPageIntroProps) => { + let visualHeaderType = null; + + // Video takes precedence + if (videoUrl) { + visualHeaderType = "video"; + } else if (image) { + visualHeaderType = "image"; + } else { + visualHeaderType = null; + } + + return ( + + + {variant === "featureDisabled" ? ( + + ) : null} + + {visualHeaderType === "video" ? ( + + + + ) : null} + + {visualHeaderType === "image" ? ( + + ) : null} + + + {title} + + + + ( +
      + {children} +
    + ), + li: ({ children }) => ( +
  • {children}
  • + ), + a: ({ children, href }) => ( + { + logrocketTrackIfEnabled("blank_slate_docs_link_clicked", { + link: href, + }); + + return true; + }} + target="_blank" + rel="noopener noreferrer" + style={{ color: colors.blue[700], textDecoration: "none" }} + > + {children} + + ), + }} + > + {message} +
    +
    + + {(primaryAction || secondaryAction) && ( + + {primaryAction && ( + + )} + {secondaryAction && ( + + )} + + )} + + {footer && ( + + {footer} + + )} +
    +
    + ); +}; + +export default EmptyPageIntro; diff --git a/ui-next/src/components/ErrorBoundary.tsx b/ui-next/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000000..850f296ce3 --- /dev/null +++ b/ui-next/src/components/ErrorBoundary.tsx @@ -0,0 +1,77 @@ +import { Component, ErrorInfo, ReactNode } from "react"; +import { Box } from "@mui/material"; +// import { reportErrorToHeap, isHeapEnabled } from "utils"; + +import { reportErrorToLogRocket, isLogRocketEnabled } from "utils"; +interface Props { + children?: ReactNode; + location?: any; +} + +interface State { + hasError: boolean; +} + +class ErrorBoundary extends Component { + public state: State = { + hasError: false, + }; + + public static getDerivedStateFromError(_: Error): State { + // Update state so the next render will show the fallback UI. + return { hasError: true }; + } + + public componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error("Uncaught error:", error, errorInfo); + + // if (isHeapEnabled()) { + // reportErrorToHeap(error); + // } + if (isLogRocketEnabled()) { + reportErrorToLogRocket(error); + } + } + + componentDidUpdate(prevProps: Props) { + if (prevProps?.location?.pathname !== this.props.location?.pathname) { + this.setState({ hasError: false }); + } + } + + public render() { + if (this.state.hasError) { + return ( + + + There was an error performing this action. Please try again. + + + Contact support if the error persists. + + + ); + } + + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/ui-next/src/components/FeatureDisabledComponent.tsx b/ui-next/src/components/FeatureDisabledComponent.tsx new file mode 100644 index 0000000000..8e39abb079 --- /dev/null +++ b/ui-next/src/components/FeatureDisabledComponent.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import EmptyPageIntro from "./EmptyPageIntro"; +import { openInNewTab } from "utils/helpers"; +import UnlockIcon from "./v1/icons/UnlockIcon"; + +const TALK_TO_AN_EXPERT_URL = "https://orkes.io/talk-to-an-expert"; + +const FeatureDisabledComponent = ({ + image, + title, + message, +}: { + image?: string; + title?: string; + message?: string; +}) => { + return ( + openInNewTab(TALK_TO_AN_EXPERT_URL), + startIcon: , + }} + /> + ); +}; + +export default FeatureDisabledComponent; diff --git a/ui-next/src/components/FeatureDisabledWrapper.tsx b/ui-next/src/components/FeatureDisabledWrapper.tsx new file mode 100644 index 0000000000..300d83dcb4 --- /dev/null +++ b/ui-next/src/components/FeatureDisabledWrapper.tsx @@ -0,0 +1,69 @@ +import { Box } from "@mui/material"; +import { ReactNode } from "react"; +import { useLocation } from "react-router"; +import FeatureDisabledComponent from "./FeatureDisabledComponent"; +import { checkPathFlag } from "utils/checkPathFlag"; +import { colors } from "theme/tokens/variables"; + +const textStyle = { + fontSize: "14px", + fontWeight: 700, + color: colors.sidebarBlacky, + a: { + color: colors.primary, + textDecoration: "none", + }, +}; + +const featureDisabled = (path: string) => { + const flagValue = checkPathFlag(path); + return flagValue ? false : true; +}; + +export function FeatureDisabledWrapper({ + featureDisabledCustomComponent, + children, +}: { + children: ReactNode; + featureDisabledCustomComponent?: ReactNode; +}) { + const { pathname } = useLocation(); + + return ( + + {featureDisabled(pathname) ? ( + featureDisabledCustomComponent ? ( + featureDisabledCustomComponent + ) : ( + + ) + ) : ( + {children} + )} + + ); +} + +export function FeatureDisabledHeader() { + return ( + + {"Your trial has ended. Please "} + + contact us + + {" or "} + + upgrade your cluster + + . + + ); +} diff --git a/ui-next/src/components/FloatingMuiAlert.tsx b/ui-next/src/components/FloatingMuiAlert.tsx new file mode 100644 index 0000000000..2cfe8924d5 --- /dev/null +++ b/ui-next/src/components/FloatingMuiAlert.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import { Alert, AlertTitle, styled } from "@mui/material"; + +const StyledAlert = styled(Alert)(() => ({ + backgroundColor: "#E8F5E9", + color: "#1B5E20", + "& .MuiAlert-icon": { + color: "#1B5E20", + }, + border: "1px solid #A5D6A7", + borderRadius: "4px", + padding: "6px 16px", + "& .MuiAlert-message": { + padding: "8px 0", + fontSize: "14px", + color: "rgba(37, 37, 37, 1)", + }, + boxShadow: "4px 4px 10px 0px rgba(89, 89, 89, 0.41)", + position: "fixed", + top: "16px", + right: "16px", + zIndex: 1400, + "& .MuiAlertTitle-root": { + color: "black", + fontWeight: 600, + marginBottom: "2px", + }, +})); + +interface FloatingMuiAlertProps { + title?: string; + message?: string; + onClose?: () => void; +} + +const FloatingMuiAlert: React.FC = ({ + title = "Congratulations! You've created a workflow!", + message = "Edit whatever you want, or not, and take it for a Run!", + onClose, +}) => { + return ( + + {title} + {message} + + ); +}; + +export default FloatingMuiAlert; diff --git a/ui-next/src/components/GetStartedSample/GetStartedSample.tsx b/ui-next/src/components/GetStartedSample/GetStartedSample.tsx new file mode 100644 index 0000000000..1d16cc6e06 --- /dev/null +++ b/ui-next/src/components/GetStartedSample/GetStartedSample.tsx @@ -0,0 +1,158 @@ +import { useState } from "react"; +import { Button, Grid, Stack } from "@mui/material"; +import DownloadIcon from "@mui/icons-material/Download"; +import { Input, Tab, Tabs } from "components"; +import { + CodeLanguage, + JavaLanguageSet, + OperatingSystemEnvironment, +} from "./types"; +import { useConductorProjectBuilder } from "utils/hooks/useConductorProjectBuilder"; +import { CodeSnippet } from "./components/CodeSnippet"; + +export const DEFAULT_TASK_NAME = "my_first_simple_task"; + +export const GetStartedSample = ({ + serverUrl = "your-server-url-goes-here", + onTaskNameUpdated, +}: { + apiKey?: string; + apiSecret?: string; + serverUrl?: string; + environment: OperatingSystemEnvironment; + onTaskNameUpdated?: (taskName: string) => void; +}) => { + const [selectedLanguage, setSelectedLanguage] = useState( + CodeLanguage.JAVA, + ); + + const [selectedJavaLanguageSet, setSelectedJavaLanguageSet] = + useState(JavaLanguageSet.GRADLE); + const [taskName, setTaskName] = useState(DEFAULT_TASK_NAME); + const [projectName, setProjectName] = useState( + "ConductorSampleProject", + ); + const [packageName, setPackageName] = useState("org.example"); + const [namespace, setNamespace] = useState(""); + + const { displayCode, onDownload } = useConductorProjectBuilder({ + serverUrl, + taskName: taskName || DEFAULT_TASK_NAME, + language: selectedLanguage, + languageSet: selectedJavaLanguageSet, + namespace, + packageName, + projectName, + useEnvVars: true, + }); + + return ( + <> + setSelectedLanguage(val)} + > + {Object.values(CodeLanguage) + .filter( + (item) => + item !== CodeLanguage.CLOJURE && item !== CodeLanguage.GROOVY, + ) + .map((item) => ( + + ))} + + {selectedLanguage === CodeLanguage.JAVA && ( + setSelectedJavaLanguageSet(val)} + > + {Object.values(JavaLanguageSet).map((item) => ( + + ))} + + )} + + + + + + { + setTaskName(value); + onTaskNameUpdated?.(value); + }} + /> + + + {selectedLanguage === CodeLanguage.JAVA && ( + + { + setProjectName(value); + }} + /> + + )} + + {[CodeLanguage.JAVA, CodeLanguage.GROOVY].includes( + selectedLanguage, + ) && ( + + { + setPackageName(value); + }} + /> + + )} + + {[CodeLanguage.CSHARP].includes(selectedLanguage) && ( + + { + setNamespace(value); + }} + /> + + )} + + + + + + + + + + + + ); +}; diff --git a/ui-next/src/components/GetStartedSample/components/CodeSnippet.tsx b/ui-next/src/components/GetStartedSample/components/CodeSnippet.tsx new file mode 100644 index 0000000000..c6cb7a432d --- /dev/null +++ b/ui-next/src/components/GetStartedSample/components/CodeSnippet.tsx @@ -0,0 +1,58 @@ +import { useState } from "react"; +import { Box, Button, Stack } from "@mui/material"; +import Highlight from "react-highlight"; + +export const CodeSnippet = ({ + code, + className, + noCopyToClipboard, +}: { + code: string; + className?: string; + noCopyToClipboard?: boolean; +}) => { + const [buttonText, setButtonText] = useState("Copy"); + + const handleCopy = () => { + navigator.clipboard.writeText(code); + setButtonText("Copied!"); + setTimeout(() => { + setButtonText("Copy"); + }, 1000); + }; + + return ( + + {code} + {!noCopyToClipboard && ( + + + + )} + + ); +}; diff --git a/ui-next/src/components/GetStartedSample/types.ts b/ui-next/src/components/GetStartedSample/types.ts new file mode 100644 index 0000000000..36c86a864e --- /dev/null +++ b/ui-next/src/components/GetStartedSample/types.ts @@ -0,0 +1,19 @@ +export enum OperatingSystemEnvironment { + MAC = "MacOs/Linux", + WINDOWS = "Windows", +} + +export enum CodeLanguage { + JAVA = "Java", + PYTHON = "Python", + GO = "Golang", + CSHARP = "CSharp", + JS = "JavaScript", + CLOJURE = "Clojure", + GROOVY = "Groovy", +} + +export enum JavaLanguageSet { + GRADLE = "Gradle", + SPRING_GRADLE = "Spring + Gradle", +} diff --git a/ui-next/src/components/Header.tsx b/ui-next/src/components/Header.tsx new file mode 100644 index 0000000000..599d4c3ec5 --- /dev/null +++ b/ui-next/src/components/Header.tsx @@ -0,0 +1,5 @@ +import LinearProgress from "components/LinearProgress"; + +export default function Header({ loading }: { loading: boolean }) { + return
    {loading && }
    ; +} diff --git a/ui-next/src/components/Heading.jsx b/ui-next/src/components/Heading.jsx new file mode 100644 index 0000000000..962c4345f5 --- /dev/null +++ b/ui-next/src/components/Heading.jsx @@ -0,0 +1,9 @@ +import MuiTypography from "./MuiTypography"; + +const levelMap = ["h6", "h5", "h4", "h3", "h2", "h1"]; + +const Heading = ({ level = 3, ...props }) => { + return ; +}; + +export default Heading; diff --git a/ui-next/src/components/HelperText.jsx b/ui-next/src/components/HelperText.jsx new file mode 100644 index 0000000000..305d4d893c --- /dev/null +++ b/ui-next/src/components/HelperText.jsx @@ -0,0 +1,11 @@ +import { Box } from "@mui/material"; + +const HelperText = ({ children }) => { + return ( + + {children} + + ); +}; + +export default HelperText; diff --git a/ui-next/src/components/InlineEdit.tsx b/ui-next/src/components/InlineEdit.tsx new file mode 100644 index 0000000000..7faf16545c --- /dev/null +++ b/ui-next/src/components/InlineEdit.tsx @@ -0,0 +1,139 @@ +import { Box, IconButton, TextField, Theme } from "@mui/material"; +import { + X as Cancel, + Check, + PencilSimple as EditIcon, +} from "@phosphor-icons/react"; +import _isEmpty from "lodash/isEmpty"; +import { ReactNode, useEffect, useState } from "react"; + +const additionalWidth = 15; + +export const InlineEdit = ({ + value, + editLabel = , + saveLabel = , + cancelLabel = , + flexGrow = 0, + onSave, + onChangeMode, + error = false, + helperText, + notAllowedCharRegex, + disabled = false, +}: { + value: string; + editLabel?: ReactNode; + saveLabel?: ReactNode; + cancelLabel?: ReactNode; + flexGrow?: number; + onSave: (val: string) => void; + onChangeMode?: (edit: boolean) => void; + error?: boolean; + helperText?: string; + notAllowedCharRegex?: RegExp; + disabled?: boolean; +}) => { + const [edit, setEdit] = useState(false); + const [internalValue, setInternalValue] = useState(""); + + useEffect(() => { + setInternalValue(value); + }, [value]); + + useEffect(() => { + if (onChangeMode) { + onChangeMode(edit); + } + }, [edit, onChangeMode]); + + const handleInputChange = (newValue: string) => { + if (notAllowedCharRegex) { + if (!notAllowedCharRegex.test(newValue)) { + setInternalValue(newValue); + } + } else { + setInternalValue(newValue); + } + }; + + const disableSave = _isEmpty(internalValue?.trim()); + + return ( + + {edit ? ( + + handleInputChange(e.target.value)} + sx={{ + width: `${internalValue.length + additionalWidth}ch`, + minWidth: "120px", + maxWidth: "480px", + }} + error={error} + helperText={helperText} + > + + ) : ( + + error + ? { + border: `2px solid ${theme.palette.error.main}`, + borderRadius: "4px", + padding: 1, + } + : {} + } + > + {internalValue} + + )} + + {edit ? ( + <> + + { + if (internalValue !== value) { + onSave(internalValue); + } + setEdit(false); + }} + disabled={disableSave} + > + {saveLabel} + + + + { + setInternalValue(value); + setEdit(false); + }} + > + {cancelLabel} + + + + ) : ( + setEdit(true)} + sx={{ cursor: "pointer", marginTop: "-6px" }} + disabled={disabled} + > + {editLabel} + + )} + + + ); +}; diff --git a/ui-next/src/components/Input.tsx b/ui-next/src/components/Input.tsx new file mode 100644 index 0000000000..8c8f912326 --- /dev/null +++ b/ui-next/src/components/Input.tsx @@ -0,0 +1,106 @@ +import InputAdornment from "@mui/material/InputAdornment"; +import TextField, { TextFieldProps } from "@mui/material/TextField"; +import { X as ClearIcon } from "@phosphor-icons/react"; +import IconButton from "components/MuiIconButton"; +import { ChangeEvent, forwardRef, useImperativeHandle, useRef } from "react"; +import { disabledInputStyle } from "shared/styles"; + +export type CustomInputProps = Omit & { + clearable?: boolean; + onBlur?: (value: string) => void; + onChange?: (value: string) => void; +}; + +const CustomInput = forwardRef( + ( + { + label = null, + clearable, + onBlur = () => null, + onChange = () => null, + value, + ...props + }: CustomInputProps, + ref, + ) => { + const inputRef = useRef(null); + useImperativeHandle(ref, () => inputRef?.current, [inputRef]); + + function handleClear(_e: any) { + if (inputRef.current?.value) { + inputRef.current.value = ""; + } + + if (onBlur) { + onBlur(""); + } + + if (onChange) { + onChange(""); + } + } + + function handleBlur( + e: ChangeEvent, + ) { + if (onBlur) { + const { value } = e.target; + + onBlur(value); + } + } + + function handleChange( + e: ChangeEvent, + ) { + if (onChange) { + const { value } = e.target; + + onChange(value); + } + } + + return ( + + + theme.palette.mode === "dark" ? "#fff" : null, + }} + > + + + + ) : undefined, + autoFocus: props.autoFocus, + }} + onBlur={handleBlur} + onChange={handleChange} + value={value} + {...props} + /> + ); + }, +); + +export default CustomInput; diff --git a/ui-next/src/components/InputNumber.tsx b/ui-next/src/components/InputNumber.tsx new file mode 100644 index 0000000000..a078d4a81d --- /dev/null +++ b/ui-next/src/components/InputNumber.tsx @@ -0,0 +1,81 @@ +import { TextField, TextFieldProps } from "@mui/material"; +import _isEmpty from "lodash/isEmpty"; +import _isNil from "lodash/isNil"; +import { ChangeEvent, FunctionComponent, KeyboardEvent } from "react"; +import { disabledInputStyle } from "shared/styles"; +import { logger } from "utils/logger"; + +type InputNumberProps = Omit & { + onChange: (val: number | null, event: ChangeEvent) => void; +}; +const pattern = /^(0|[1-9]\d*)?$/; +const isaValidNumber = (value: string) => pattern.test(value); + +function removeNonMatchingChars(str: string) { + let result = ""; + for (let i = 0; i < str.length; i++) { + if (pattern.test(str[i])) { + result += str[i]; + } + } + return result; +} + +/** + * The requirement for this component was + * "number" : null, + * "number" : 0, + * "number" : 10 + * Meaning allow empty. and set to null if empty. no leading 0s + * @param param0 + * @returns + */ +export const InputNumber: FunctionComponent = ({ + onChange, + value, + ...restProps +}) => { + const handleInputChange = (event: ChangeEvent) => { + const incomingValue = event.target.value; + const isValidNumber = isaValidNumber(incomingValue); + if (onChange && isValidNumber) { + onChange(_isEmpty(incomingValue) ? null : Number(incomingValue), event); + } else if (!isValidNumber) { + const result = removeNonMatchingChars(incomingValue); + onChange(_isEmpty(result) ? null : Number(result), event); + } + + if (restProps.type === "number") { + logger.warn( + "Setting type to number on InputNumber will not allow to add 0", + ); + } + }; + const handleOnKeyDown = (e: KeyboardEvent) => { + if ( + e.ctrlKey || + e.shiftKey || + e.key === "Backspace" || + e.key === "Enter" || + e.key === "Tab" || + e.key === "Delete" || + e.key === "ArrowLeft" || + e.key === "ArrowRight" || + e.key === "ArrowUp" || + e.key === "ArrowDown" + ) + return; + if (!isaValidNumber(e.key)) { + e.preventDefault(); + } + }; + return ( + + ); +}; diff --git a/ui-next/src/components/IntegrationIcon.tsx b/ui-next/src/components/IntegrationIcon.tsx new file mode 100644 index 0000000000..2843747ba6 --- /dev/null +++ b/ui-next/src/components/IntegrationIcon.tsx @@ -0,0 +1,31 @@ +import { FunctionComponent } from "react"; + +interface IntegrationIconProps { + integrationName?: string; + size?: number; +} + +export const IntegrationIcon: FunctionComponent = ({ + integrationName, + size = 24, +}) => { + // Check if the integrationName is a URL (starts with http:// or https://) + const isUrl = integrationName?.match(/^https?:\/\//i); + + return ( + {integrationName} { + // Only fall back to default if it's not a URL + if (!isUrl) { + currentTarget.onerror = null; + currentTarget.src = `/integrations-icons/default.svg`; + } + }} + /> + ); +}; diff --git a/ui-next/src/components/IntegrationsIcon.tsx b/ui-next/src/components/IntegrationsIcon.tsx new file mode 100644 index 0000000000..b4ac82f440 --- /dev/null +++ b/ui-next/src/components/IntegrationsIcon.tsx @@ -0,0 +1,16 @@ +const SvgComponent = (props: any) => ( + + + +); +export default SvgComponent; diff --git a/ui-next/src/components/KeyValueTable.jsx b/ui-next/src/components/KeyValueTable.jsx new file mode 100644 index 0000000000..4b3b679b56 --- /dev/null +++ b/ui-next/src/components/KeyValueTable.jsx @@ -0,0 +1,112 @@ +import { Grid } from "@mui/material"; +import _isNil from "lodash/isNil"; + +import { useEnv } from "plugins/env"; +import { durationRenderer, timestampRenderer } from "utils/index"; +import { customTypeRenderers } from "plugins/customTypeRenderers"; +import StatusBadge from "components/StatusBadge"; +import Paper from "./Paper"; + +export default function KeyValueTable({ data }) { + const env = useEnv(); + return ( + + {data.map((item, index) => { + let displayValue; + const renderer = item.type ? customTypeRenderers[item.type] : null; + if (renderer) { + displayValue = renderer(item.value, data, env); + } else { + switch (item.type) { + case "date": + displayValue = + !isNaN(item.value) && item.value > 0 + ? timestampRenderer(item.value) + : "N/A"; + break; + case "duration": + displayValue = + !isNaN(item.value) && item.value > 0 + ? durationRenderer(item.value) + : "N/A"; + break; + + case "status": + displayValue = ; + break; + + default: + displayValue = !_isNil(item.value) ? item.value : "N/A"; + } + } + + return ( + + + + {item.label} + + + {item.type === "error" ? ( + + {item.value} + + ) : ( + displayValue + )} + + + + ); + })} + + ); +} diff --git a/ui-next/src/components/LinearProgress.tsx b/ui-next/src/components/LinearProgress.tsx new file mode 100644 index 0000000000..00657f3a8d --- /dev/null +++ b/ui-next/src/components/LinearProgress.tsx @@ -0,0 +1,16 @@ +import { default as MuiLinearProgress } from "@mui/material/LinearProgress"; + +export default function LinearProgress({ sx = {}, ...props }) { + return ( + + ); +} diff --git a/ui-next/src/components/MetricsChart.tsx b/ui-next/src/components/MetricsChart.tsx new file mode 100644 index 0000000000..3c001a2d3c --- /dev/null +++ b/ui-next/src/components/MetricsChart.tsx @@ -0,0 +1,42 @@ +import { HistoricalData } from "types/MetricsTypes"; +import { + CacheChart, + ChartType, + ErrorsChart, + LatencyChart, + RequestsChart, +} from "./charts"; + +interface MetricsChartProps { + type: ChartType; + historicalData?: HistoricalData[]; + visiblePercentiles?: Record; +} + +export function MetricsChart({ + type, + historicalData = [], + visiblePercentiles = { p50: true, p95: true, p99: true }, +}: MetricsChartProps) { + switch (type) { + case ChartType.REQUESTS: + return ; + + case ChartType.LATENCY: + return ( + + ); + + case ChartType.ERRORS: + return ; + + case ChartType.CACHE: + return ; + + default: + return null; + } +} diff --git a/ui-next/src/components/MuiAlert.tsx b/ui-next/src/components/MuiAlert.tsx new file mode 100644 index 0000000000..ea6ef05f24 --- /dev/null +++ b/ui-next/src/components/MuiAlert.tsx @@ -0,0 +1,15 @@ +import Alert, { AlertProps } from "@mui/material/Alert"; +import { CSSProperties, forwardRef } from "react"; + +interface MuiAlertProps extends AlertProps { + style?: CSSProperties; +} + +const MuiAlert = forwardRef( + ({ style, ...props }, ref) => { + return ; + }, +); + +export default MuiAlert; +export type { MuiAlertProps }; diff --git a/ui-next/src/components/MuiButton.tsx b/ui-next/src/components/MuiButton.tsx new file mode 100644 index 0000000000..20cc71c92f --- /dev/null +++ b/ui-next/src/components/MuiButton.tsx @@ -0,0 +1,4 @@ +import MuiButton, { ButtonProps as MuiButtonProps } from "@mui/material/Button"; + +export type { MuiButtonProps }; +export default MuiButton; diff --git a/ui-next/src/components/MuiButtonGroup.tsx b/ui-next/src/components/MuiButtonGroup.tsx new file mode 100644 index 0000000000..42de19e254 --- /dev/null +++ b/ui-next/src/components/MuiButtonGroup.tsx @@ -0,0 +1,6 @@ +import MuiButtonGroup, { + ButtonGroupProps as MuiButtonGroupProps, +} from "@mui/material/ButtonGroup"; + +export type { MuiButtonGroupProps }; +export default MuiButtonGroup; diff --git a/ui-next/src/components/MuiCheckbox.tsx b/ui-next/src/components/MuiCheckbox.tsx new file mode 100644 index 0000000000..9069f6dd3a --- /dev/null +++ b/ui-next/src/components/MuiCheckbox.tsx @@ -0,0 +1,6 @@ +import MuiCheckbox, { + CheckboxProps as MuiCheckboxProps, +} from "@mui/material/Checkbox"; + +export default MuiCheckbox; +export type { MuiCheckboxProps }; diff --git a/ui-next/src/components/MuiIconButton.tsx b/ui-next/src/components/MuiIconButton.tsx new file mode 100644 index 0000000000..f79635c582 --- /dev/null +++ b/ui-next/src/components/MuiIconButton.tsx @@ -0,0 +1,6 @@ +import MuiIconButton, { + IconButtonProps as MuiIconButtonProps, +} from "@mui/material/IconButton"; + +export type { MuiIconButtonProps }; +export default MuiIconButton; diff --git a/ui-next/src/components/MuiTypography.tsx b/ui-next/src/components/MuiTypography.tsx new file mode 100644 index 0000000000..19a7ed7090 --- /dev/null +++ b/ui-next/src/components/MuiTypography.tsx @@ -0,0 +1,31 @@ +import Typography, { TypographyProps } from "@mui/material/Typography"; +import { CSSProperties, ElementType, FC } from "react"; + +interface MuiTypographyProps extends TypographyProps { + style?: CSSProperties; + opacity?: number; + textDecoration?: "overline" | "line-through" | "underline"; + cursor?: string; + component?: ElementType; +} + +const MuiTypography: FC = ({ + style, + opacity, + textDecoration, + cursor, + sx, + ...props +}) => { + const customStyles: CSSProperties = { + ...style, + opacity, + textDecoration, + cursor, + }; + + return ; +}; + +export default MuiTypography; +export type { MuiTypographyProps }; diff --git a/ui-next/src/components/NavLink.jsx b/ui-next/src/components/NavLink.jsx new file mode 100644 index 0000000000..06ac0af57c --- /dev/null +++ b/ui-next/src/components/NavLink.jsx @@ -0,0 +1,41 @@ +import { Link } from "@mui/material"; +import { ArrowSquareOut } from "@phosphor-icons/react"; +import { useEnv } from "plugins/env"; +import { forwardRef } from "react"; +import { Link as RouterLink } from "react-router"; +import Url from "url-parse"; + +// 1. Strip `navigate` from props to prevent error +// 2. Preserve stack param +const NavLink = forwardRef((props, ref) => { + const { path, newTab, ...rest } = props; + const { stack, defaultStack } = useEnv(); + + const url = new Url(path, {}, true); + if (stack !== defaultStack) { + url.query.stack = stack; + } + + if (!newTab) { + return ( + + {rest.children} + + ); + } else { + return ( + + {rest.children} +   + + + ); + } +}); + +export default NavLink; diff --git a/ui-next/src/components/NavLink.tsx b/ui-next/src/components/NavLink.tsx new file mode 100644 index 0000000000..aecb5aa418 --- /dev/null +++ b/ui-next/src/components/NavLink.tsx @@ -0,0 +1,50 @@ +import { Link } from "@mui/material"; +import { ArrowSquareOut } from "@phosphor-icons/react"; +import { useEnv } from "plugins/env"; +import { CSSProperties, ForwardedRef, ReactNode, forwardRef } from "react"; +import { Link as RouterLink } from "react-router"; +import Url from "url-parse"; + +interface NavLinkProps { + path: string; + newTab?: boolean; + children: ReactNode; + id?: string; + style?: CSSProperties; + target?: string; + color?: string; +} + +const NavLink = forwardRef((props: NavLinkProps, ref: ForwardedRef) => { + const { path, newTab, ...rest } = props; + const { stack, defaultStack } = useEnv(); + + const url = new Url(path, {}, true); + if (stack !== defaultStack) { + url.query.stack = stack; + } + + if (!newTab) { + return ( + + {rest.children} + + ); + } else { + return ( + + {rest.children} +   + + + ); + } +}); +NavLink.displayName = "NavLink"; + +export default NavLink; diff --git a/ui-next/src/components/NoDataComponent.tsx b/ui-next/src/components/NoDataComponent.tsx new file mode 100644 index 0000000000..40fd95a46f --- /dev/null +++ b/ui-next/src/components/NoDataComponent.tsx @@ -0,0 +1,65 @@ +import React from "react"; +import EmptyPageIntro, { EmptyPageIntroProps } from "./EmptyPageIntro"; +import TagChip from "./TagChip"; +import { Box } from "@mui/material"; + +type NoDataComponentProps = { + id?: string; + title?: string; + titleBg?: string; + description: string; + buttonText?: string; + buttonHandler?: () => void; + disableButton?: boolean; + videoUrl?: string; +}; + +const NoDataComponent = ({ + id, + title, + titleBg, + buttonText, + buttonHandler, + description, + disableButton = false, + videoUrl, +}: NoDataComponentProps) => { + const props: Omit = { + id, + message: description, + videoUrl, + ...(buttonText && { + primaryAction: { + text: buttonText, + onClick: buttonHandler || (() => {}), + disabled: disableButton, + }, + }), + }; + + return ( + + ) : ( + + {title || "Empty"} + + ) + } + /> + ); +}; + +export type { NoDataComponentProps }; +export default NoDataComponent; diff --git a/ui-next/src/components/PanelAccordion.tsx b/ui-next/src/components/PanelAccordion.tsx new file mode 100644 index 0000000000..c8160d24ad --- /dev/null +++ b/ui-next/src/components/PanelAccordion.tsx @@ -0,0 +1,86 @@ +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import { + Accordion, + AccordionDetails, + AccordionProps, + AccordionSummary, + SxProps, + Theme, + Typography, + alpha, +} from "@mui/material"; +import { ReactNode, useState } from "react"; + +const ACCORDION_HEIGHT = 51; + +export const PanelAccordion = ({ + children, + sx = {}, + title, + defaultExpanded = false, + ...rest +}: { + children: ReactNode; + sx?: SxProps; + title: ReactNode; + defaultExpanded?: boolean; +} & AccordionProps) => { + const [isExpanded, setIsExpanded] = useState(defaultExpanded); + + return ( + setIsExpanded(!isExpanded)} + sx={{ + "&.Mui-expanded": { + margin: 0, + }, + ...sx, + }} + {...rest} + > + } + sx={{ + px: 5, + minHeight: ACCORDION_HEIGHT, + "&.Mui-expanded": { + minHeight: ACCORDION_HEIGHT, + width: "100%", + bgcolor: "#F3FBFF", + }, + "&:hover": { + bgcolor: "#F3FBFF", + }, + "& .MuiAccordionSummary-content": { + display: "flex", + alignItems: "center", + justifyContent: "space-between", + width: "100%", + }, + "& .MuiAccordionSummary-content.Mui-expanded": { + margin: 0, + }, + }} + > + + {title} + + + + {children} + + + ); +}; diff --git a/ui-next/src/components/Paper.tsx b/ui-next/src/components/Paper.tsx new file mode 100644 index 0000000000..e48db9a0a2 --- /dev/null +++ b/ui-next/src/components/Paper.tsx @@ -0,0 +1,18 @@ +import MuiPaper, { PaperProps } from "@mui/material/Paper"; +import { forwardRef, Ref } from "react"; + +const Paper = forwardRef(function ( + { elevation, ...props }: PaperProps, + ref: Ref, +) { + return ( + -1 ? elevation : 0} + style={{ borderRadius: 4 }} + {...props} + /> + ); +}); +Paper.displayName = "Paper"; +export default Paper; diff --git a/ui-next/src/components/PromptVariables.tsx b/ui-next/src/components/PromptVariables.tsx new file mode 100644 index 0000000000..29f1259951 --- /dev/null +++ b/ui-next/src/components/PromptVariables.tsx @@ -0,0 +1,59 @@ +import { Grid } from "@mui/material"; +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import { ConductorFlatMapFormBase } from "components/v1/FlatMapForm/ConductorFlatMapForm"; +import { TaskDef } from "types"; + +type PromptVariablesProps = { + currentVariables: string | Record; + onChange: (t: Partial) => void; + updateField: ( + path: string, + value: unknown, + task: Partial, + ) => Partial; + task: Partial; +}; + +const PromptVariables = ({ + currentVariables, + onChange, + updateField, + task, +}: PromptVariablesProps) => { + return ( + <> + {typeof currentVariables === "string" ? ( + + + onChange( + updateField(`inputParameters.promptVariables`, value, task), + ) + } + value={currentVariables} + label="Prompt variables" + /> + + ) : ( + + ) => + onChange( + updateField(`inputParameters.promptVariables`, value, task), + ) + } + value={{ ...currentVariables }} + autoFocusField={false} + /> + + )} + + ); +}; + +export default PromptVariables; diff --git a/ui-next/src/components/Puller.tsx b/ui-next/src/components/Puller.tsx new file mode 100644 index 0000000000..b8257e03b2 --- /dev/null +++ b/ui-next/src/components/Puller.tsx @@ -0,0 +1,17 @@ +import { styled } from "@mui/material"; +import { grey } from "@mui/material/colors"; + +const Puller = styled("div")(({ theme }) => ({ + width: 30, + height: 5, + backgroundColor: grey[400], + borderRadius: 3, + position: "absolute", + top: 8, + left: "calc(50% - 15px)", + ...theme.applyStyles("dark", { + backgroundColor: grey[900], + }), +})); + +export default Puller; diff --git a/ui-next/src/components/RadioButtonGroup.tsx b/ui-next/src/components/RadioButtonGroup.tsx new file mode 100644 index 0000000000..5d861b4a64 --- /dev/null +++ b/ui-next/src/components/RadioButtonGroup.tsx @@ -0,0 +1,66 @@ +import { Box } from "@mui/material"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import Radio from "@mui/material/Radio"; +import RadioGroup from "@mui/material/RadioGroup"; +import { ChangeEvent } from "react"; +import { colors } from "theme/tokens/variables"; + +export interface RadioButtonGroupProp { + ariaLabel?: string; + items: { + disabled?: boolean; + value: string | number; + label: string; + helperText?: string; + }[]; + name: string; + onChange?: (evt: ChangeEvent, val: string) => void; + value?: string | number; +} + +const RadioButtonGroup = ({ + ariaLabel, + items, + name, + onChange, + value, +}: RadioButtonGroupProp) => { + return ( + + {items.map((item, index) => ( + } + id={`${item.label}-radio-btn`} + label={ + + <>{item.label} + {item.helperText && ( + + {item.helperText} + + )} + + } + disabled={item.disabled} + /> + ))} + + ); +}; + +export default RadioButtonGroup; diff --git a/ui-next/src/components/ReactJson.tsx b/ui-next/src/components/ReactJson.tsx new file mode 100644 index 0000000000..a998aacf7a --- /dev/null +++ b/ui-next/src/components/ReactJson.tsx @@ -0,0 +1,349 @@ +import Editor, { Monaco } from "@monaco-editor/react"; +import { Box, Paper, Tooltip } from "@mui/material"; +import { + CornersOut, + Download, + List, + ListPlus, + PencilSimple, + XCircle, +} from "@phosphor-icons/react"; +import Button from "components/MuiButton"; +import SaveIcon from "components/v1/icons/SaveIcon"; +import XCloseIcon from "components/v1/icons/XCloseIcon"; +import { CSSProperties, Suspense, useContext, useRef, useState } from "react"; +import { defaultEditorOptions, type EditorOptions } from "shared/editor"; +import { ColorModeContext } from "theme/material/ColorModeContext"; +import { colors } from "theme/tokens/variables"; +import { tryToJson } from "utils/utils"; + +const DARK_BACKGROUND = "#111111"; +const COLLAPSE_IDLE = "COLLAPSE_IDLE"; +const COLLAPSE_EXPAND = "COLLAPSE_EXPAND"; +const COLLAPSE_COLLAPSE = "COLLAPSE_COLLAPSE"; + +export interface ReactJSONProps { + src: any; + title?: string; + className?: string; + style?: CSSProperties; + showIconText?: boolean; + workflowName?: string; + editorHeight?: string; + item?: any; + handleFullScreen?: (item: any) => void; + fullScreen?: any; + customOptions?: object; + overflowX?: string; + overflowY?: string; + isEditable?: boolean; + handleUpdate?: (value: string) => void; +} + +const editorOptions: EditorOptions = { + ...defaultEditorOptions, + tabSize: 2, + readOnly: true, + quickSuggestions: true, + folding: true, + automaticLayout: true, + scrollbar: { + // this property is added because it was not allowing us to scroll when mouse pointer is over this component + alwaysConsumeMouseWheel: false, + }, + wordWrap: "on", +}; + +export default function ReactJson({ + title, + className = "", + style, + showIconText = true, + editorHeight = "500px", + handleFullScreen, + item, + fullScreen, + customOptions, + overflowX, + overflowY, + isEditable, + handleUpdate, + ...props +}: ReactJSONProps) { + const editorRef = useRef(null); + + const [collapse, setCollapse] = useState(COLLAPSE_EXPAND); + const [editEnabled, setEditEnabled] = useState(false); + const [isJsonParsable, setIsJsonParsable] = useState(true); + const colorModeContext = useContext(ColorModeContext); + let mode = "light"; + if (colorModeContext && colorModeContext.mode) { + mode = colorModeContext.mode; + } + + const handleFoldAll = () => { + const editor = editorRef.current; + if (editor) { + const foldAction = editor.getAction("editor.foldAll"); + foldAction.run(); + } + }; + + const handleUnfoldAll = () => { + const editor = editorRef.current; + if (editor) { + const unfoldAction = editor.getAction("editor.unfoldAll"); + unfoldAction.run(); + } + }; + + const toggleCollapse = () => { + const shouldExpand = [COLLAPSE_IDLE, COLLAPSE_COLLAPSE].includes(collapse); + + if (shouldExpand) { + handleUnfoldAll(); + setCollapse(COLLAPSE_EXPAND); + } else { + handleFoldAll(); + setCollapse(COLLAPSE_COLLAPSE); + } + }; + + const toggleDownload = () => { + const a = window.document.createElement("a"); + a.href = window.URL.createObjectURL( + new Blob([JSON.stringify(props.src, null, 2)], { + type: "application/json", + }), + ); + a.download = `${props.workflowName}_${title}.json`; + + // Append anchor to body. + document.body.appendChild(a); + a.click(); + + // Remove anchor from body + document.body.removeChild(a); + }; + + const toggleFullscreen = () => { + if (handleFullScreen && item) { + handleFullScreen(item); + } + }; + + const collapseButtonText = + collapse === COLLAPSE_IDLE || collapse === COLLAPSE_COLLAPSE + ? "Expand all" + : "Collapse all"; + + const handleEditorWillMount = (monaco: Monaco) => { + monaco.editor.defineTheme("vs-light", { + base: "vs", + inherit: true, + rules: [ + { + token: "number", + foreground: colors.primaryGreen, + }, + ], + colors: {}, + }); + }; + + const handleEditorMount = (editor: Monaco) => { + editorRef.current = editor; + }; + + const mainStyle: object = { + ...style, + ...(overflowX && { overflowX: overflowX }), + ...(overflowY && { overflowY: overflowY }), + }; + + const handleEnableEdit = (value: boolean) => { + setEditEnabled(value); + }; + + const onEditorChange = () => { + const editorValue = editorRef?.current?.getValue(); + const tryJson = tryToJson(editorValue); + if (tryJson) { + setIsJsonParsable(true); + } else { + setIsJsonParsable(false); + } + }; + + const handleSave = () => { + const editorValue = editorRef?.current?.getValue(); + setEditEnabled(false); + if (handleUpdate) { + handleUpdate(editorValue); + } + }; + + return ( + + + + {title} + + + + {isEditable && ( + <> + {!editEnabled ? ( + + + + ) : ( + + + + + + )} + + )} + + + + + + + + + {fullScreen && ( + + )} + + + + + Loading...}> + + + + + ); +} diff --git a/ui-next/src/components/RoleTagChip.tsx b/ui-next/src/components/RoleTagChip.tsx new file mode 100644 index 0000000000..4bbcbbb27a --- /dev/null +++ b/ui-next/src/components/RoleTagChip.tsx @@ -0,0 +1,36 @@ +import { ChipProps } from "@mui/material"; +import { userRoleColorGenerator } from "utils/roles"; +import { forwardRef } from "react"; +import { toUpperFirst } from "utils"; +import TagChip from "./TagChip"; + +const RoleTagChip = forwardRef( + ({ style = {}, label = "", ...props }, ref) => { + let combinedStyles; + if (typeof label === "string") { + combinedStyles = { + ...userRoleColorGenerator(label), + ...style, + }; + } else { + combinedStyles = { ...style }; + } + const formattedLabel = () => { + if (typeof label === "string") { + return toUpperFirst(label); + } + + return label; + }; + return ( + + ); + }, +); + +export default RoleTagChip; diff --git a/ui-next/src/components/SafariWarning.tsx b/ui-next/src/components/SafariWarning.tsx new file mode 100644 index 0000000000..4f0969f236 --- /dev/null +++ b/ui-next/src/components/SafariWarning.tsx @@ -0,0 +1,20 @@ +import { SnackbarMessage } from "components/SnackbarMessage"; +import { isSafari } from "utils"; + +interface SafariWarningProps { + setShowSafariWarning: (show: boolean) => void; +} + +export const SafariWarning = ({ setShowSafariWarning }: SafariWarningProps) => { + if (isSafari) { + return ( + { + setShowSafariWarning(false); + }} + /> + ); + } +}; diff --git a/ui-next/src/components/SearchEverything.tsx b/ui-next/src/components/SearchEverything.tsx new file mode 100644 index 0000000000..c5c34a3b7c --- /dev/null +++ b/ui-next/src/components/SearchEverything.tsx @@ -0,0 +1,311 @@ +import SearchIcon from "@mui/icons-material/Search"; +import { Box } from "@mui/material"; +import InputBase from "@mui/material/InputBase"; +import { CaretDoubleRight, XCircle } from "@phosphor-icons/react"; +import _first from "lodash/fp/first"; +import _isEqual from "lodash/isEqual"; +import { ReactElement, useCallback, useEffect, useMemo, useState } from "react"; +import { useNavigate } from "react-router"; +import { blueLight, seGrey, seGrey2 } from "theme/tokens/colors"; +import useArrowNavigation, { + useArrowNavigationProps, +} from "useArrowNavigation"; + +type SearchResultBase = { + icon?: ReactElement; + title: string; + route?: string; +}; + +type SearchResultRoute = SearchResultBase & { + sub?: never; +}; + +type SearchResultSub = SearchResultBase & { + sub: SearchResults; +}; +type SearchResultItem = SearchResultRoute | SearchResultSub; + +type SearchResults = Array; + +export interface SearchEverythingProps { + onChange: (change: string, max?: number) => void; + searchResults?: SearchResults; + onClear: () => void; + searchTerm: string; + setOpen?: (value: boolean) => void; + maxSearchResults?: number; +} + +const searchBarStyle = { + padding: "13px", + borderRadius: "11px", + border: `1px solid ${blueLight}`, + display: "flex", + alignItems: "center", +}; +const closeCircleStyle = { + marginLeft: "auto", + display: "flex", + alignItems: "center", + cursor: "pointer", +}; +const searchInputStyle = { + padding: "0 8px", + width: "100%", + input: { + fontSize: "14px", + fontStyle: "normal", + fontWeight: 500, + lineHeight: "normal", + }, +}; +const resultsWrapperStyle = { + padding: "8px 0", +}; +const resultGroupStyle = { + padding: "8px 0", +}; +const resultTitleStyle = { + fontSize: "14px", + fontStyle: "normal", + fontWeight: 600, + lineHeight: "normal", + color: blueLight, + padding: "8px 0", + cursor: "pointer", +}; + +const noResultWrapper = { + display: "flex", + flexDirection: "column", + justifyContent: "center", + alignItems: "center", + padding: "50px 0px 25px 0px", +}; +const titleWithBg = { + display: "flex", + alignItems: "center", + height: "60px", + backgroundImage: `url(searchIconBg.svg)`, + backgroundSize: "80px 80px", + backgroundRepeat: "no-repeat", + backgroundPosition: "center", +}; +const noResultTitle = { + color: "#060606", + fontWeight: 600, + fontSize: "16px", + marginTop: "-15px", +}; +const noResultSuggestion = { + color: blueLight, + fontSize: "12px", + lineHeight: "16px", + fontWeight: 700, + textTransform: "uppercase", + display: "flex", + alignItems: "center", + padding: "2px 0", + cursor: "pointer", +}; +const uniqueKeyGenerator = (index: number, subIndex: number, title: string) => { + return `${index}-${subIndex}-${title}`; +}; + +const useSearchEverythingHook = (props: useArrowNavigationProps) => { + const firstOptionItemHash = useMemo(() => { + const head = _first(props?.options); + if (head) { + return props?.optionsIdGen(head); + } + return undefined; + }, [props]); + + const [higlightedElement, setHighlighetedElement] = useState( + firstOptionItemHash ?? "", + ); + + useEffect(() => { + if (firstOptionItemHash !== undefined && firstOptionItemHash !== "") { + setHighlighetedElement(firstOptionItemHash); + } + }, [firstOptionItemHash]); + + const arrowNavProps = useArrowNavigation({ + ...props, + hoveredItem: higlightedElement, + setHoveredItem: setHighlighetedElement, + }); + return arrowNavProps; +}; + +function SearchEverything({ + onChange, + searchResults, + onClear, + searchTerm, + setOpen, + maxSearchResults, +}: SearchEverythingProps) { + const navigate = useNavigate(); + + const searchItems: SearchResultBase[] = useMemo(() => { + const result = + searchResults?.map( + (item) => + item.sub?.map((subItem) => { + return subItem; + }) || [], + ) ?? []; + if (result && result.length > 0) { + return result.flat(); + } + return []; + }, [searchResults]); + + const optionsIdGen = useCallback((sr: SearchResultItem) => { + return `${sr.route?.replace("/", "_")}`; + }, []); + + const { inputProps, optionPropsForItem, hoveredItem } = + useSearchEverythingHook({ + onSelect: (elem) => { + handleRedirect(elem); + }, + options: searchItems || [], + optionsIdGen, + scrollToCenter: true, + hoveredItem: "", + setHoveredItem: () => {}, + }); + + const subTitleStyle = (item: string) => { + return { + transition: "all 0.3s ease", + borderRadius: "6px", + background: _isEqual(item, hoveredItem) ? blueLight : seGrey, + color: _isEqual(item, hoveredItem) ? "#FFFFFF" : "000000", + padding: "12px 24px", + fontSize: "14px", + fontWeight: 500, + lineHeight: "normal", + fontStyle: "normal", + margin: "2px 0", + cursor: "pointer", + display: "flex", + alignItems: "center", + "&:hover #enter-icon": { + visibility: "visible", + }, + }; + }; + const enterIconStyle = (item: string) => { + return { + marginLeft: "auto", + visibility: _isEqual(item, hoveredItem) ? "visible" : "hidden", + }; + }; + const handleChangeText = (value: string) => { + onChange(value, maxSearchResults); + }; + + const handleRedirect = (sub: SearchResultItem) => { + if (sub.route) { + navigate(sub.route); + if (setOpen) { + setOpen(false); + } + } + }; + + return ( + + + + + + + handleChangeText(e.target.value)} + {...inputProps} + /> + + onClear()}> + + + + {/* search result not found */} + {searchResults && searchResults.length === 0 && ( + + + {`No results for "${searchTerm}"`} + + + Try searching for: + + + handleChangeText("workflow")} + > + Workflow names + + handleChangeText("task")} + > + Task definitions + + + + )} + {/* search results found */} + {searchResults && searchResults.length > 0 && ( + + {searchResults.map( + (item, index) => + item.sub && + item.sub.length > 0 && ( + + handleRedirect(item)} + > + {item.title} + + {item.sub && + item.sub.length > 0 && + item.sub.map((subItem, subIndex) => ( + handleRedirect(subItem)} + > + {subItem.title} + + enter-icon + + + ))} + + ), + )} + + )} + + ); +} + +export default SearchEverything; diff --git a/ui-next/src/components/Select.tsx b/ui-next/src/components/Select.tsx new file mode 100644 index 0000000000..35d583bca0 --- /dev/null +++ b/ui-next/src/components/Select.tsx @@ -0,0 +1,39 @@ +import { + FormControl, + InputLabel, + Select as MuiSelect, + SelectProps as MuiSelectProps, +} from "@mui/material"; +import _isNil from "lodash/isNil"; +import { CSSProperties, ReactNode } from "react"; + +interface SelectProps extends Omit { + label?: ReactNode; + fullWidth?: boolean; + nullable?: boolean; + style?: CSSProperties; + renderValue?: (value: unknown) => ReactNode; +} + +const Select = ({ + label, + fullWidth, + nullable = true, + style, + ...props +}: SelectProps) => { + return ( + + {label && {label}} + (_isNil(v) ? "" : (v as ReactNode))} + {...props} + /> + + ); +}; + +export default Select; diff --git a/ui-next/src/components/Sidebar/BaseSubMenu.tsx b/ui-next/src/components/Sidebar/BaseSubMenu.tsx new file mode 100644 index 0000000000..7154fdbb99 --- /dev/null +++ b/ui-next/src/components/Sidebar/BaseSubMenu.tsx @@ -0,0 +1,92 @@ +import Collapse from "@mui/material/Collapse"; +import List from "@mui/material/List"; +import { useContext, useMemo } from "react"; +import { colors } from "theme/tokens/variables"; +import { SidebarContext } from "./context/SidebarContext"; +import { SidebarItem } from "./SidebarItem"; +import { SubMenuProps } from "./types"; + +export const BaseSubMenu = ({ items, parentId }: SubMenuProps) => { + const { open, openedMenus, location } = useContext(SidebarContext); + const isSubMenuOpen = openedMenus?.some((menu) => menu === parentId); + + const treeStyle = useMemo(() => { + const isActive = items.some((item) => item.linkTo === location?.pathname); + if (open) { + if (isActive) { + return { + height: "22px", + top: "-4px", + }; + } + + return { + height: "32px", + top: "-14px", + }; + } + + return { + height: "26px", + top: "-8px", + }; + }, [items, location?.pathname, open]); + + return ( + + .MuiListItem-root": { + "&:first-of-type": { + ".MuiButtonBase-root": { + // mt: 1, + ":before": treeStyle, + }, + }, + + ".MuiButtonBase-root": { + color: colors.sidebarGreyText, + py: 0, + transition: " background-color 0.3s ease-in-out", + + ":before": { + ml: 1, + content: "''", + borderLeft: `1px solid ${colors.sidebarFaintGrey}`, + position: "absolute", + width: "10px", + height: "36px", + top: "-20px", + left: "15px", + }, + + ".MuiBox-root": { + ml: 7, + }, + + ".MuiListItemText-root": { + ".MuiListItemText-primary": { + fontSize: 12, + }, + }, + }, + }, + }} + > + {items.map((item) => ( + + ))} + + + ); +}; diff --git a/ui-next/src/components/Sidebar/ClosedLogo.tsx b/ui-next/src/components/Sidebar/ClosedLogo.tsx new file mode 100644 index 0000000000..967dc874b6 --- /dev/null +++ b/ui-next/src/components/Sidebar/ClosedLogo.tsx @@ -0,0 +1,24 @@ +import OrkesIcon from "images/svg/orkes-icon.svg"; +import { featureFlags, FEATURES } from "utils/flags"; + +// Logos +const ConductorOSSLogo = "https://assets.conductor-oss.org/logo.png"; + +// Determine which logo to use based on ACCESS_MANAGEMENT feature flag +// Enterprise (ACCESS_MANAGEMENT enabled) uses Orkes icon +// OSS (ACCESS_MANAGEMENT disabled) uses Conductor OSS logo +const isEnterprise = featureFlags.isEnabled(FEATURES.ACCESS_MANAGEMENT); +const defaultLogo = isEnterprise ? OrkesIcon : ConductorOSSLogo; +const defaultAltText = isEnterprise ? "Orkes logo" : "Conductor OSS logo"; + +export const ClosedLogo = ({ customLogo }: { customLogo?: string }) => ( + {defaultAltText} +); diff --git a/ui-next/src/components/Sidebar/HotKeysButton.tsx b/ui-next/src/components/Sidebar/HotKeysButton.tsx new file mode 100644 index 0000000000..4d2c0467d3 --- /dev/null +++ b/ui-next/src/components/Sidebar/HotKeysButton.tsx @@ -0,0 +1,79 @@ +import Chip from "@mui/material/Chip"; +import Stack from "@mui/material/Stack"; +import { ReactNode, useMemo } from "react"; + +// Detect if the user is on Windows +const isWindows = () => { + return ( + /Win/i.test(navigator.platform) || /Windows/i.test(navigator.userAgent) + ); +}; + +// Convert shortcut display based on OS +const formatShortcut = ( + shortcut: ReactNode, + isWindowsOS: boolean, +): ReactNode => { + if (typeof shortcut === "string") { + // Replace ⌘ with Ctrl on Windows + if (isWindowsOS) { + return shortcut.replace(/⌘/g, "Ctrl"); + } + return shortcut; + } + return shortcut; +}; + +export default function HotKeysButton({ + shortcuts, +}: { + shortcuts: ReactNode[]; +}) { + const isWindowsOS = useMemo(() => isWindows(), []); + + const formattedShortcuts = useMemo( + () => shortcuts.map((shortcut) => formatShortcut(shortcut, isWindowsOS)), + [shortcuts, isWindowsOS], + ); + + return ( + + {formattedShortcuts.map((item, index) => ( + + ))} + + ); +} diff --git a/ui-next/src/components/Sidebar/OpenedLogo.tsx b/ui-next/src/components/Sidebar/OpenedLogo.tsx new file mode 100644 index 0000000000..b3bbc9f03c --- /dev/null +++ b/ui-next/src/components/Sidebar/OpenedLogo.tsx @@ -0,0 +1,72 @@ +import Stack, { StackProps } from "@mui/material/Stack"; +import OrkesLogo from "images/svg/orkes-logo.svg"; +import { featureFlags, FEATURES } from "utils/flags"; + +// Logos +const ConductorOSSLogo = "https://assets.conductor-oss.org/logo.png"; + +// Determine which logo to use based on ACCESS_MANAGEMENT feature flag +// Enterprise (ACCESS_MANAGEMENT enabled) uses Orkes logo +// OSS (ACCESS_MANAGEMENT disabled) uses Conductor OSS logo +const isEnterprise = featureFlags.isEnabled(FEATURES.ACCESS_MANAGEMENT); +const defaultLogo = isEnterprise ? OrkesLogo : ConductorOSSLogo; +const defaultAltText = isEnterprise ? "Orkes logo" : "Conductor OSS logo"; + +export const OpenedLogo = ({ + customLogo, + ...rest +}: StackProps & { + customLogo?: string; +}) => ( + + {customLogo ? ( + {`Powered + ) : ( + {defaultAltText} + )} + +); diff --git a/ui-next/src/components/Sidebar/RunWorkflowButton.tsx b/ui-next/src/components/Sidebar/RunWorkflowButton.tsx new file mode 100644 index 0000000000..d8722d3213 --- /dev/null +++ b/ui-next/src/components/Sidebar/RunWorkflowButton.tsx @@ -0,0 +1,59 @@ +import PlayIcon from "@mui/icons-material/PlayArrowOutlined"; +import { Box } from "@mui/material"; + +import MuiButton from "components/MuiButton"; +import MuiIconButton from "components/MuiIconButton"; +import { usePushHistory } from "utils/hooks/usePushHistory"; +import { RUN_WORKFLOW_URL } from "utils/constants/route"; +import { useAuth } from "shared/auth"; + +const RunWorkflowButton = ({ open }: { open: boolean }) => { + const pushHistory = usePushHistory(); + const { isTrialExpired } = useAuth(); + + if (!open) { + return ( + + pushHistory(RUN_WORKFLOW_URL)} + sx={{ + opacity: "0.7", + fontSize: "18px", + ":hover": { + color: "white", + backgroundColor: "transparent", + opacity: 1, + }, + }} + > + + + + ); + } + + return ( + + } + onClick={() => pushHistory(RUN_WORKFLOW_URL)} + disabled={isTrialExpired} + > + Run Workflow + + + ); +}; + +export default RunWorkflowButton; diff --git a/ui-next/src/components/Sidebar/Sidebar.tsx b/ui-next/src/components/Sidebar/Sidebar.tsx new file mode 100644 index 0000000000..6511ed2db8 --- /dev/null +++ b/ui-next/src/components/Sidebar/Sidebar.tsx @@ -0,0 +1,203 @@ +import { Backdrop, Box, Drawer, alpha, useTheme } from "@mui/material"; +import { useMemo, useState } from "react"; +import { useAuth } from "shared/auth"; +import { colors } from "theme/tokens/variables"; +import { Auth0User } from "types/User"; +import { drawerWidthClose, drawerWidthOpen } from "./constants"; +import { useSidebarHover } from "./hooks/useSidebarHover"; +import { SidebarHeader } from "./SidebarHeader"; +import { SidebarMenu } from "./SidebarMenu"; +import { SidebarToggleButton } from "./SidebarToggleButton"; +import { MenuItemType } from "./types"; + +interface SidebarProps { + menuItems: MenuItemType[]; + open?: boolean; + onToggle?: (open: boolean) => void; + apiVersion?: string; + releaseVersion?: string; + isAnnouncementBannerVisible?: boolean; + customLogo?: string; + isMobile?: boolean; + toggleMenu?: () => void; + onSearchClick?: () => void; +} + +export const Sidebar = ({ + menuItems, + open: controlledOpen, + onToggle, + apiVersion, + releaseVersion, + isAnnouncementBannerVisible: _isAnnouncementBannerVisible, + customLogo, + isMobile = false, + toggleMenu, + onSearchClick, +}: SidebarProps) => { + const theme = useTheme(); + const [internalOpen, setInternalOpen] = useState(true); + const { user, logOut, conductorUser, isAuthenticated } = useAuth(); + const [showCopyAlert, setShowCopyAlert] = useState(false); + + const { + hoveredMenuId, + getItemRef, + handleMouseEnter, + handleMouseLeave, + handlePopoverMouseEnter, + handlePopoverMouseLeave, + } = useSidebarHover(); + + // Use controlled state if provided, otherwise use internal state + const open = controlledOpen !== undefined ? controlledOpen : internalOpen; + + const handleToggle = () => { + if (toggleMenu) { + // Use toggleMenu if provided (for controlled state) + toggleMenu(); + } else if (onToggle) { + // Use onToggle if provided (takes boolean) + onToggle(!open); + } else { + // Fall back to internal state + setInternalOpen(!open); + } + }; + + const [conductorVersion, uiVersion]: string[] = useMemo( + () => [apiVersion || "latest", releaseVersion || "latest"], + [apiVersion, releaseVersion], + ); + + const visibleItems = useMemo( + () => menuItems.filter((item) => !item.hidden), + [menuItems], + ); + + // Group items into sections + const sections = useMemo(() => { + const mainItems: MenuItemType[] = []; + + visibleItems.forEach((item) => { + if (item.items && item.items.length > 0) { + // Items with children are their own sections + mainItems.push(item); + } else { + // Regular items go to main + mainItems.push(item); + } + }); + + return mainItems; + }, [visibleItems]); + + const drawerCustomHeight = useMemo(() => { + if (isMobile) { + return "100vh"; + } else { + return _isAnnouncementBannerVisible ? "calc(100vh - 45px)" : "100vh"; + } + }, [_isAnnouncementBannerVisible, isMobile]); + + const topMarginForChevronIcon = useMemo(() => { + if (!_isAnnouncementBannerVisible) { + return open ? "18px" : "50px"; + } else { + return open ? "63px" : "95px"; + } + }, [_isAnnouncementBannerVisible, open]); + + const sidebarContent = ( + + + + + + ); + + // Wrap in Drawer for mobile, otherwise return as-is + return (isMobile && open) || !isMobile ? ( + <> + + + {sidebarContent} + + {isMobile && ( + theme.zIndex.drawer - 1, + backdropFilter: "blur(2px)", + }} + open={!!open} + onClick={toggleMenu} + /> + )} + + ) : null; +}; + +export default Sidebar; diff --git a/ui-next/src/components/Sidebar/SidebarFooter.tsx b/ui-next/src/components/Sidebar/SidebarFooter.tsx new file mode 100644 index 0000000000..084711d818 --- /dev/null +++ b/ui-next/src/components/Sidebar/SidebarFooter.tsx @@ -0,0 +1,293 @@ +import { LogoutOutlined } from "@mui/icons-material"; +import { + Avatar, + Box, + Button, + IconButton, + Tooltip, + Typography, + alpha, + useTheme, +} from "@mui/material"; +import { SnackbarMessage } from "components/SnackbarMessage"; +import { SidebarVersionBlock } from "./SidebarVersionBlock"; +import TokenIcon from "images/svg/token.svg"; +import { getAccessToken } from "shared/auth/tokenManagerJotai"; +import { Auth0User } from "types/User"; +import { FEATURES, featureFlags } from "utils"; +import type { ReactNode } from "react"; + +const isPlayground = featureFlags.isEnabled(FEATURES.PLAYGROUND); + +interface SidebarFooterProps { + open: boolean; + isAuthenticated: boolean; + isMobile: boolean; + user: Auth0User | null; + conductorUser: { id: string } | null; + logOut?: () => void; + conductorVersion: string; + uiVersion: string; + showCopyAlert: boolean; + setShowCopyAlert: (show: boolean) => void; + /** When provided (e.g. by enterprise), used for user/sign-out block; version still shown below. */ + customUserBlock?: ReactNode; +} + +export const SidebarFooter = ({ + open, + isAuthenticated, + isMobile, + user, + conductorUser, + logOut, + conductorVersion, + uiVersion, + showCopyAlert, + setShowCopyAlert, + customUserBlock, +}: SidebarFooterProps) => { + const theme = useTheme(); + + if (customUserBlock != null) { + return ( + <> + {customUserBlock} + {open && ( + + + + )} + + ); + } + + return ( + <> + {/* Footer with Signout Button when collapsed */} + {!open && isAuthenticated && !isMobile && ( + + + { + if (logOut) { + logOut(); + } + }} + size="small" + sx={{ + color: theme.palette.text.secondary, + "&:hover": { + backgroundColor: alpha(theme.palette.action.hover, 0.08), + color: theme.palette.text.primary, + }, + }} + > + + + + + )} + + {/* Footer with UserInfo and Version */} + {open && ( + + {/* User Info, Signout and Copy Token */} + {isAuthenticated && ( + + {/* User Avatar and Info */} + + + + + {(user as Auth0User)?.given_name} + + + {conductorUser?.id} + + + + { + if (logOut) { + logOut(); + } + }} + size="small" + sx={{ + color: theme.palette.text.secondary, + ml: "auto", + }} + > + + + + + + {/* Copy Token Button */} + + {/* Spacer for avatar */} + + {(() => { + const copyTokenButton = ( + + ); + + return isPlayground ? ( + + {copyTokenButton} + + ) : ( + copyTokenButton + ); + })()} + {/* Spacer for signout button */} + + + + )} + + {showCopyAlert && ( + setShowCopyAlert(false)} + anchorOrigin={{ horizontal: "right", vertical: "bottom" }} + /> + )} + + + + )} + + ); +}; diff --git a/ui-next/src/components/Sidebar/SidebarHeader.tsx b/ui-next/src/components/Sidebar/SidebarHeader.tsx new file mode 100644 index 0000000000..a4b33b7828 --- /dev/null +++ b/ui-next/src/components/Sidebar/SidebarHeader.tsx @@ -0,0 +1,105 @@ +import CloseIcon from "@mui/icons-material/Close"; +import SearchIcon from "@mui/icons-material/Search"; +import { Box, IconButton, Typography } from "@mui/material"; +import { useMemo } from "react"; +import { Key } from "ts-key-enum"; +import { ClosedLogo } from "./ClosedLogo"; +import { OpenedLogo } from "./OpenedLogo"; +import { SidebarItem } from "./SidebarItem"; +import { MenuItemType } from "./types"; + +interface SidebarHeaderProps { + open: boolean; + isMobile: boolean; + customLogo?: string; + toggleMenu?: () => void; + onSearchClick?: () => void; +} + +export const SidebarHeader = ({ + open, + isMobile, + customLogo, + toggleMenu, + onSearchClick, +}: SidebarHeaderProps) => { + // Create search item for SidebarItem component + const searchItem: MenuItemType = useMemo( + () => ({ + id: "searchItem", + title: "Search", + icon: , + shortcuts: ["⌘", "K"], + hotkeys: `${Key.Meta} + K`, + handler: onSearchClick, + hidden: false, + }), + [onSearchClick], + ); + + return ( + + {/* Logo or MENU text */} + {isMobile ? ( + + + MENU + + + + + + ) : ( + <> + + {open ? ( + + ) : ( + + )} + + {/* Search item on new line */} + {onSearchClick && ( + + + + )} + + )} + + ); +}; diff --git a/ui-next/src/components/Sidebar/SidebarItem.tsx b/ui-next/src/components/Sidebar/SidebarItem.tsx new file mode 100644 index 0000000000..615f4fa69d --- /dev/null +++ b/ui-next/src/components/Sidebar/SidebarItem.tsx @@ -0,0 +1,435 @@ +import ChevronRightIcon from "@mui/icons-material/ChevronRight"; +import { + Box, + ClickAwayListener, + Collapse, + List, + ListItem, + ListItemButton, + ListItemIcon, + ListItemText, + Paper, + Popper, + alpha, + useTheme, +} from "@mui/material"; +import React, { + ReactNode, + createElement, + useCallback, + useMemo, + useRef, + useState, +} from "react"; +import { useHotkeys } from "react-hotkeys-hook"; +import { Link, matchPath, useLocation } from "react-router"; +import { colors } from "theme/tokens/variables"; +import { HOT_KEYS_SIDEBAR } from "utils/constants/common"; +import HotKeysButton from "./HotKeysButton"; +import { MenuItemType } from "./types"; + +interface SidebarItemProps { + item: MenuItemType; + level?: number; + open?: boolean; + isActive?: boolean; + onItemClick?: (item: MenuItemType) => void; + hoveredMenuId?: string | null; + onMouseEnter?: () => void; + onMouseLeave?: () => void; + onPopoverMouseEnter?: () => void; + onPopoverMouseLeave?: () => void; + itemRef?: React.RefObject; +} + +export const SidebarItem = ({ + item, + level = 0, + open = true, + isActive = false, + onItemClick, + hoveredMenuId, + onMouseEnter, + onMouseLeave, + onPopoverMouseEnter, + onPopoverMouseLeave, + itemRef, +}: SidebarItemProps) => { + const location = useLocation(); + const theme = useTheme(); + + const hasChildren = item.items && item.items.length > 0; + const isRouteActive = useMemo(() => { + if (item.linkTo && location.pathname === item.linkTo) return true; + if (item.activeRoutes) { + return item.activeRoutes.some((route) => + matchPath({ path: route, end: true }, location.pathname), + ); + } + return false; + }, [item.linkTo, item.activeRoutes, location.pathname]); + + const visibleChildren = useMemo( + () => item.items?.filter((child) => !child.hidden) || [], + [item.items], + ); + + // Auto-expand if any child is active + const hasActiveChild = useMemo(() => { + return visibleChildren.some((child) => { + if (child.linkTo && location.pathname === child.linkTo) return true; + if (child.activeRoutes) { + return child.activeRoutes.some((route) => + matchPath({ path: route, end: true }, location.pathname), + ); + } + return false; + }); + }, [visibleChildren, location.pathname]); + + // Initialize expanded state - all menus default expanded + const [isExpanded, setIsExpanded] = useState(true); + + // Update expanded state when active child changes + const prevHasActiveChildRef = useRef(hasActiveChild); + + if (hasActiveChild && !prevHasActiveChildRef.current && !isExpanded) { + setIsExpanded(true); + } + prevHasActiveChildRef.current = hasActiveChild; + + // Parent items should show as active if any child is active + const active = Boolean( + isActive || isRouteActive || (hasChildren && hasActiveChild), + ); + // Check if this is a parent with an active child (not directly active) + const isParentWithActiveChild = + hasChildren && hasActiveChild && !isRouteActive && !isActive; + + const handleClick = useCallback(() => { + if (hasChildren) { + setIsExpanded((prev) => !prev); + } + if (onItemClick) { + onItemClick(item); + } + if (item.handler) { + item.handler(); + } + }, [hasChildren, item, onItemClick]); + + // Handle keyboard shortcuts + useHotkeys( + item.hotkeys || "", + (event) => { + if (!item.hotkeys || item.hotkeys.trim() === "") return; + event.preventDefault(); + if (item.handler) { + item.handler(); + } else if (item.linkTo && !hasChildren) { + window.location.href = item.linkTo; + } + }, + { + scopes: HOT_KEYS_SIDEBAR, + enableOnFormTags: ["INPUT", "TEXTAREA", "SELECT"], + }, + ); + + if (item.hidden) return null; + + const isSearchItem = item.id === "searchItem"; + + // Generic badge: items can supply a useBadgeCount hook to show reactive counts. + // Enterprise plugins register items with useBadgeCount (e.g. for human task inbox). + // We call item.useBadgeCount if present, otherwise fall back to a no-op hook. + const badgeCount = (item.useBadgeCount ?? (() => 0))(); + const showBadge = badgeCount > 0; + + // Check if link should open in new tab (isOpenNewTab flag or absolute URL) + const isExternalLink = + item.isOpenNewTab || + (item.linkTo && + (item.linkTo.startsWith("//") || + item.linkTo.startsWith("http://") || + item.linkTo.startsWith("https://"))); + + const itemContent = ( + 0 ? 32 : isSearchItem ? 24 : 32, + borderRadius: isSearchItem ? 8 : 1, + mx: open ? (isSearchItem ? 1.5 : 1) : 0.5, + mb: level > 0 ? 0.5 : isSearchItem ? 1.5 : 0.75, + mt: isSearchItem ? 1.5 : 0, + px: open ? (level > 0 ? 1.25 : isSearchItem ? 2 : 1.5) : 1, + py: level > 0 ? 0.75 : isSearchItem ? 1.25 : 1, + justifyContent: open ? "flex-start" : "center", + position: "relative", + transition: "all 0.2s ease-in-out", + backgroundColor: isSearchItem + ? alpha(theme.palette.action.hover, 0.05) + : isParentWithActiveChild + ? alpha(theme.palette.action.hover, 0.05) + : active + ? alpha(theme.palette.primary.main, 0.1) + : "transparent", + color: isSearchItem + ? alpha(theme.palette.text.primary, 0.8) + : isParentWithActiveChild + ? theme.palette.text.primary + : active + ? theme.palette.primary.main + : alpha(theme.palette.text.primary, 0.7), + boxShadow: isSearchItem + ? `0 1px 2px ${alpha(theme.palette.common.black, 0.05)}` + : "none", + "&:hover": { + backgroundColor: isSearchItem + ? alpha(theme.palette.action.hover, 0.08) + : isParentWithActiveChild + ? alpha(theme.palette.action.hover, 0.08) + : active + ? alpha(theme.palette.primary.main, 0.15) + : alpha(theme.palette.action.hover, 0.05), + borderColor: isSearchItem + ? alpha(theme.palette.divider, 0.5) + : "transparent", + boxShadow: isSearchItem + ? `0 2px 4px ${alpha(theme.palette.common.black, 0.08)}` + : "none", + color: isSearchItem + ? theme.palette.text.primary + : isParentWithActiveChild + ? theme.palette.text.primary + : active + ? theme.palette.primary.main + : theme.palette.text.primary, + }, + ...(level > 0 && + open && { + ml: 2, + pl: 2, + fontSize: "0.8125rem", + }), + }} + > + {item.icon && ( + 0 ? 28 : isSearchItem ? 40 : 28) : "auto", + justifyContent: open ? "flex-start" : "center", + color: "inherit", + marginRight: open && level === 0 && !isSearchItem ? 0.5 : 0, + "& svg": { + fontSize: level > 0 ? 16 : isSearchItem ? 22 : 20, + }, + }} + > + {item.icon} + + )} + {open && ( + <> + + {item.title} + + {badgeCount} + + + ) : ( + item.title + ) + } + primaryTypographyProps={{ + fontSize: + level > 0 + ? "0.8125rem" + : isSearchItem + ? "0.9375rem" + : "0.9375rem", + fontWeight: isSearchItem ? 600 : active ? 600 : 500, + sx: { + transition: "font-weight 0.2s ease-in-out", + lineHeight: level > 0 ? 1.3 : 1.5, + }, + }} + /> + {item.shortcuts && item.shortcuts.length > 0 && ( + + )} + {hasChildren && ( + + )} + + )} + + ); + + // If item has a custom component, render it but still show children if it has them + const hasCustomComponent = + item.component && typeof item.component !== "function"; + const hasCustomComponentFunction = + item.component && typeof item.component === "function"; + + const isHovered = hoveredMenuId === item.id; + const showPopper = + !open && + hasChildren && + level === 0 && + isHovered && + !hasCustomComponent && + !hasCustomComponentFunction; + + return ( + <> + {hasCustomComponentFunction && typeof item.component === "function" ? ( + <> + {createElement(item.component, { + isParent: Boolean(hasChildren), + isTopParent: level === 0, + isRouteActive: isRouteActive, + isTopParentActive: level === 0 && isRouteActive, + active: active, + maybeActiveStyles: {}, + icon: item.icon, + })} + + ) : hasCustomComponent ? ( + {item.component as ReactNode} + ) : ( + } + disablePadding + sx={{ display: "block", position: "relative" }} + > + {itemContent} + + )} + {hasChildren && open && ( + + + {visibleChildren.map((child) => ( + + + + ))} + + + )} + {showPopper && itemRef?.current && ( + {})}> + + + + {visibleChildren.map((child) => ( + + + + ))} + + + + + )} + + ); +}; diff --git a/ui-next/src/components/Sidebar/SidebarMenu.tsx b/ui-next/src/components/Sidebar/SidebarMenu.tsx new file mode 100644 index 0000000000..dd5a45f1ca --- /dev/null +++ b/ui-next/src/components/Sidebar/SidebarMenu.tsx @@ -0,0 +1,137 @@ +import { Box, List, Tooltip, alpha, useTheme } from "@mui/material"; +import { ReactNode, RefObject } from "react"; +import { Auth0User } from "types/User"; +import { SidebarItem } from "./SidebarItem"; +import { SidebarFooter } from "./SidebarFooter"; +import { MenuItemType } from "./types"; + +interface SidebarMenuProps { + sections: MenuItemType[]; + open: boolean; + hoveredMenuId: string | null; + getItemRef: (itemId: string) => RefObject; + handleMouseEnter: (itemId: string) => () => void; + handleMouseLeave: () => void; + handlePopoverMouseEnter: () => void; + handlePopoverMouseLeave: () => void; + // Footer props + isAuthenticated: boolean; + isMobile: boolean; + user: Auth0User | null; + conductorUser: { id: string } | null; + logOut?: () => void; + conductorVersion: string; + uiVersion: string; + showCopyAlert: boolean; + setShowCopyAlert: (show: boolean) => void; + /** When provided (e.g. by enterprise), used for the user block so auth comes from host app; version block still shown. */ + customUserBlock?: ReactNode; +} + +export const SidebarMenu = ({ + sections, + open, + hoveredMenuId, + getItemRef, + handleMouseEnter, + handleMouseLeave, + handlePopoverMouseEnter, + handlePopoverMouseLeave, + isAuthenticated, + isMobile, + user, + conductorUser, + logOut, + conductorVersion, + uiVersion, + showCopyAlert, + setShowCopyAlert, + customUserBlock, +}: SidebarMenuProps) => { + const theme = useTheme(); + + return ( + + + {sections.map((item) => { + const hasChildren = item.items && item.items.length > 0; + const itemRef = hasChildren ? getItemRef(item.id) : undefined; + + return ( + + {open ? ( + + ) : ( + + + + + + )} + + ); + })} + + + + ); +}; diff --git a/ui-next/src/components/Sidebar/SidebarToggleButton.tsx b/ui-next/src/components/Sidebar/SidebarToggleButton.tsx new file mode 100644 index 0000000000..ee76e0dc25 --- /dev/null +++ b/ui-next/src/components/Sidebar/SidebarToggleButton.tsx @@ -0,0 +1,54 @@ +import ChevronLeftIcon from "@mui/icons-material/ChevronLeft"; +import ChevronRightIcon from "@mui/icons-material/ChevronRight"; +import { IconButton, Tooltip, alpha, useTheme } from "@mui/material"; +import { drawerWidthClose, drawerWidthOpen } from "./constants"; + +interface SidebarToggleButtonProps { + open: boolean; + isMobile: boolean; + topMargin: string; + onToggle: () => void; +} + +export const SidebarToggleButton = ({ + open, + isMobile, + topMargin, + onToggle, +}: SidebarToggleButtonProps) => { + const theme = useTheme(); + + if (isMobile) return null; + + return ( + + + {open ? : } + + + ); +}; diff --git a/ui-next/src/components/Sidebar/SidebarVersionBlock.tsx b/ui-next/src/components/Sidebar/SidebarVersionBlock.tsx new file mode 100644 index 0000000000..27f38911e2 --- /dev/null +++ b/ui-next/src/components/Sidebar/SidebarVersionBlock.tsx @@ -0,0 +1,89 @@ +import { Box, Typography, useTheme } from "@mui/material"; +import { alpha } from "@mui/material/styles"; +import ClipboardCopy from "components/ClipboardCopy"; +import { colors } from "theme/tokens/variables"; +import { FEATURES, featureFlags } from "utils"; + +const isPlayground = featureFlags.isEnabled(FEATURES.PLAYGROUND); + +interface SidebarVersionBlockProps { + open: boolean; + conductorVersion: string; + uiVersion: string; +} + +/** + * Shared version block for the sidebar footer (logo, version copy, copyright). + * Used by SidebarFooter and by SidebarMenu when rendering a custom userFooter. + */ +export function SidebarVersionBlock({ + open, + conductorVersion, + uiVersion, +}: SidebarVersionBlockProps) { + const theme = useTheme(); + + return ( + + + Conductor + + + {!isPlayground && ( + + + Orkes Platform Version + + + + + {`${conductorVersion} | ${uiVersion}`} + + + + )} + + + © Orkes Inc | All rights reserved + + + ); +} diff --git a/ui-next/src/components/Sidebar/SubMenu.tsx b/ui-next/src/components/Sidebar/SubMenu.tsx new file mode 100644 index 0000000000..258999a45e --- /dev/null +++ b/ui-next/src/components/Sidebar/SubMenu.tsx @@ -0,0 +1,88 @@ +import ClickAwayListener from "@mui/material/ClickAwayListener"; +import ListItem from "@mui/material/ListItem"; +import Popper from "@mui/material/Popper"; +import Paper from "components/Paper"; +import { useCallback, useContext, useRef } from "react"; +import { matchPath } from "react-router"; +import { colors } from "theme/tokens/variables"; +import { BaseSubMenu } from "./BaseSubMenu"; +import { SidebarContext } from "./context/SidebarContext"; +import { SidebarItem } from "./SidebarItem"; +import { SubMenuProps } from "./types"; + +export const SubMenu = (props: SubMenuProps) => { + const { open, openedMenus, setOpenedMenus, addMenu, location } = + useContext(SidebarContext); + const { id, items } = props; + const itemRef = useRef(null); + + const handlePopperClose = useCallback(() => { + setOpenedMenus([]); + }, [setOpenedMenus]); + + const handlePopperOpen = useCallback(() => { + addMenu(id); + }, [id, addMenu]); + + const isPopperOpen = openedMenus.includes(id); + const isActive = items.some( + (item) => + item.linkTo === location?.pathname || + !!item.activeRoutes?.some((route) => + matchPath({ path: route, end: true }, location?.pathname || ""), + ), + ); + + // Extract item from props (excluding items and parentId which are SubMenu-specific) + const { items: _, parentId: _parentId, ...item } = props; + + return ( + <> + + {open ? ( + + + + ) : ( + + + + + + + + )} + + ); +}; + +export default SubMenu; diff --git a/ui-next/src/components/Sidebar/UiSidebar.tsx b/ui-next/src/components/Sidebar/UiSidebar.tsx new file mode 100644 index 0000000000..a569db3cd8 --- /dev/null +++ b/ui-next/src/components/Sidebar/UiSidebar.tsx @@ -0,0 +1,203 @@ +/** + * UiSidebar - Main sidebar component for Conductor UI + * + * This component defines the core (OSS) sidebar menu items and merges in + * any additional items registered by plugins (enterprise features). + * + * Core OSS items: + * - Executions submenu (Workflow, Queue Monitor) + * - Run Workflow button + * - Definitions submenu (Workflow, Task, Event Handler) + * - API Docs + * - Help menu + * + * Enterprise items are registered via plugins and merged at runtime. + */ + +import { Sidebar } from "components/Sidebar"; +import { useAnnouncementBanner } from "components/v1/layout/header/bannerUtils"; +import { MenuItemType } from "components/Sidebar/types"; +import { pluginRegistry, SidebarItemRegistration } from "plugins/registry"; +import { FunctionComponent, useContext, useMemo } from "react"; +import { FEATURES, featureFlags } from "utils"; +import { SidebarContext } from "./context/SidebarContext"; +import { useAuth } from "../../shared/auth"; +import { getCoreSidebarItems } from "./sidebarCoreItems"; + +const customLogo = featureFlags.getValue(FEATURES.CUSTOM_LOGO_URL); + +type UISidebarProps = { + apiVersion?: string; + releaseVersion?: string; +}; + +const POSITION_END = 99999; + +/** Resolve position for sorting: number as-is, "start" => 0, "end" or undefined => end. Always returns a number. */ +function sortPosition(position: SidebarItemRegistration["position"]): number { + if (position === "start") return 0; + if (position === "end" || position === undefined) return POSITION_END; + return Number(position); +} + +/** + * Convert a plugin SidebarItemRegistration to the MenuItemType format used by the Sidebar component + */ +function pluginItemToMenuItem(item: SidebarItemRegistration): MenuItemType { + return { + id: item.id, + title: item.title, + icon: item.icon, + linkTo: item.linkTo, + activeRoutes: item.activeRoutes, + shortcuts: item.shortcuts || [], + hotkeys: item.hotkeys || "", + hidden: item.hidden ?? false, + isOpenNewTab: item.isOpenNewTab, + textStyle: item.textStyle, + buttonContainerStyle: item.buttonContainerStyle, + iconContainerStyles: item.iconContainerStyles, + handler: item.handler, + component: item.component, + position: Number(sortPosition(item.position)), + items: item.items?.map(pluginItemToMenuItem), + useBadgeCount: item.useBadgeCount, + }; +} + +/** Sort items by position (undefined last). Uses Number() so comparison is always numeric. */ +function sortItemsByPosition(items: MenuItemType[]): MenuItemType[] { + return [...items].sort( + (a, b) => + Number(a.position ?? POSITION_END) - Number(b.position ?? POSITION_END), + ); +} + +/** Recursively sort each level by position. */ +function sortMenuByPosition(items: MenuItemType[]): MenuItemType[] { + return sortItemsByPosition( + items.map((item) => + item.items?.length + ? { ...item, items: sortMenuByPosition(item.items) } + : item, + ), + ); +} + +/** + * Merge plugin-registered sidebar items into the core menu structure. + * + * Plugin items can: + * 1. Target a specific submenu (executionsSubMenu, definitionsSubMenu, etc.) to add items to it + * 2. Target "root" to add a new top-level menu item + * + * Items are inserted based on their position hint (start, end, or numeric index). + */ +function mergePluginSidebarItems( + coreItems: MenuItemType[], + pluginItems: SidebarItemRegistration[], +): MenuItemType[] { + // Clone core items to avoid mutation; explicitly preserve position for sort + const result: MenuItemType[] = coreItems.map((item) => { + const cloned: MenuItemType = { ...item, position: item.position }; + if (item.items) { + cloned.items = [...item.items]; + } + return cloned; + }); + + // Group plugin items by target menu + const itemsByTarget = new Map(); + for (const item of pluginItems) { + const target = item.targetMenu; + if (!itemsByTarget.has(target)) { + itemsByTarget.set(target, []); + } + itemsByTarget.get(target)!.push(item); + } + + // Sort items within each target by position + for (const items of itemsByTarget.values()) { + items.sort((a, b) => { + const posA = a.position ?? "end"; + const posB = b.position ?? "end"; + + if (posA === "start" && posB !== "start") return -1; + if (posB === "start" && posA !== "start") return 1; + if (posA === "end" && posB !== "end") return 1; + if (posB === "end" && posA !== "end") return -1; + + if (typeof posA === "number" && typeof posB === "number") { + return posA - posB; + } + + return 0; + }); + } + + // Insert plugin items; final order is determined by sortMenuByPosition (position 10, 15, 20, ...) + for (const [targetId, items] of itemsByTarget.entries()) { + for (const item of items) { + const menuItem = pluginItemToMenuItem(item); + if (targetId === "root") { + result.push(menuItem); + } else { + const targetMenu = result.find((i) => i.id === targetId); + if (targetMenu && targetMenu.items) { + targetMenu.items.push(menuItem); + } + } + } + } + + return sortMenuByPosition(result); +} + +export const UISidebar: FunctionComponent = ({ + apiVersion, + releaseVersion, +}) => { + const { + open, + setSearchModal, + toggleMenu, + isMobile, + isBannerOpen, + showAiStudioBanner, + } = useContext(SidebarContext); + + const { isTrialExpired, trialExpiryDate, isAnnouncementBannerDismissed } = + useAuth(); + const { showBanner } = useAnnouncementBanner( + isTrialExpired, + trialExpiryDate!, + isAnnouncementBannerDismissed, + ); + + // Get plugin-registered sidebar items + const pluginSidebarItems = useMemo( + () => pluginRegistry.getSidebarItems(), + [], + ); + + const menuItems = useMemo(() => { + const coreItems = getCoreSidebarItems(open); + return mergePluginSidebarItems(coreItems, pluginSidebarItems); + }, [open, pluginSidebarItems]); + + return ( + setSearchModal(true)} + /> + ); +}; diff --git a/ui-next/src/components/Sidebar/UserInfo.tsx b/ui-next/src/components/Sidebar/UserInfo.tsx new file mode 100644 index 0000000000..8ae0bedeb5 --- /dev/null +++ b/ui-next/src/components/Sidebar/UserInfo.tsx @@ -0,0 +1,107 @@ +import { LogoutOutlined } from "@mui/icons-material"; +import { Avatar, Box, Typography, Tooltip } from "@mui/material"; +import { useAuth } from "shared/auth"; +import { useCallback, useState } from "react"; +import { colors } from "theme/tokens/variables"; +import TokenIcon from "images/svg/token.svg"; +import { SnackbarMessage } from "components/SnackbarMessage"; +import { Auth0User } from "types/User"; +import { featureFlags, FEATURES } from "utils/flags"; +import { getAccessToken } from "shared/auth/tokenManagerJotai"; + +const isPlayground = featureFlags.isEnabled(FEATURES.PLAYGROUND); + +const UserInfo = () => { + const { user, logOut, conductorUser, isAuthenticated } = useAuth(); // Todo this should not be here since its in v1 + const [showCopyAlert, setShowCopyAlert] = useState(false); + + const handleLogout = useCallback(() => { + if (logOut) { + logOut(); + } + }, [logOut]); + + const copyTokenButton = isAuthenticated ? ( + { + setShowCopyAlert(true); + const accessToken = getAccessToken(); + if (accessToken) { + navigator.clipboard.writeText(accessToken); + } + }} + sx={{ + display: "flex", + alignItems: "center", + justifyContent: "center", + my: 4, + gap: 1, + color: colors.black, + cursor: "pointer", + fontSize: "10px", + }} + > + copyToken + Copy Token + + ) : ( +
    + ); + + return ( + + {showCopyAlert && ( + setShowCopyAlert(false)} + anchorOrigin={{ horizontal: "right", vertical: "bottom" }} + /> + )} + {isAuthenticated ? ( + + + + {(user as Auth0User | null)?.given_name} + + {conductorUser?.id} + + + + + + ) : null} + {isPlayground ? ( + + {copyTokenButton} + + ) : ( + copyTokenButton + )} + + ); +}; + +export default UserInfo; diff --git a/ui-next/src/components/Sidebar/constants.ts b/ui-next/src/components/Sidebar/constants.ts new file mode 100644 index 0000000000..f3087e5702 --- /dev/null +++ b/ui-next/src/components/Sidebar/constants.ts @@ -0,0 +1,2 @@ +export const drawerWidthOpen = 240; +export const drawerWidthClose = 52; diff --git a/ui-next/src/components/Sidebar/context/SidebarContext.tsx b/ui-next/src/components/Sidebar/context/SidebarContext.tsx new file mode 100644 index 0000000000..bbb7321615 --- /dev/null +++ b/ui-next/src/components/Sidebar/context/SidebarContext.tsx @@ -0,0 +1,46 @@ +import { ReactNode, createContext } from "react"; +import { Location } from "react-router"; +import { PersistableSidebarEvent } from "shared/PersistableSidebar/state/types"; +import { ActorRef } from "xstate"; + +export interface SidebarProviderProps { + children: ReactNode; +} + +export interface SidebarContextState { + open: boolean; + isMobile: boolean; + setOpen?: (val: boolean) => void; + openedMenus: string[]; + setOpenedMenus: (openMenus: string[]) => void; + addMenu: (id: string) => void; + removeMenu: (id: string) => void; + toggleMenu: () => void; + hideSideBar: boolean; + isSearchModalOpen: boolean; + setSearchModal: (val: boolean) => void; + isBannerOpen: boolean; + setBannerOpen: (val: boolean) => void; + location?: Location; + isStateless: boolean; + showAiStudioBanner?: boolean; + dismissAiStudioBanner?: () => void; + sidebarActor?: ActorRef; +} + +export const SidebarContext = createContext({ + isStateless: false, // If its controlled by a machine, then this is true + open: false, + isMobile: false, + setOpen: () => {}, + openedMenus: [], + setOpenedMenus: () => {}, + addMenu: () => {}, + removeMenu: () => {}, + toggleMenu: () => {}, + hideSideBar: false, + isSearchModalOpen: false, + setSearchModal: () => {}, + isBannerOpen: false, + setBannerOpen: () => {}, +}); diff --git a/ui-next/src/components/Sidebar/context/SidebarContextProvider.tsx b/ui-next/src/components/Sidebar/context/SidebarContextProvider.tsx new file mode 100644 index 0000000000..4f0d08a09e --- /dev/null +++ b/ui-next/src/components/Sidebar/context/SidebarContextProvider.tsx @@ -0,0 +1,201 @@ +import { ReactNode, useContext, useMemo, useState } from "react"; + +import { Theme } from "@mui/material/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import { useSelector } from "@xstate/react"; +import { + SidebarContext, + SidebarProviderProps, +} from "components/Sidebar/context/SidebarContext"; +import { useLocation } from "react-router"; +import { featureFlags, FEATURES } from "utils/flags"; +import { ActorRef, State } from "xstate"; +import { AuthContext } from "../../../shared/auth/context"; +import { useSidebarMenu } from "../../../shared/PersistableSidebar/state/hook"; +import { PersistableSidebarEvent } from "../../../shared/PersistableSidebar/state/types"; +import { AuthProviderMachineContext } from "../../../shared/state"; +import { + isAuthenticated as getIsAuthenticated, + noUserManagement as getIsNoUserManagement, + isSidebarInitialized, +} from "../../../shared/state/selectors"; + +const isPlayground = featureFlags.isEnabled(FEATURES.PLAYGROUND); +const isAiStudioBannerFlagOn = featureFlags.isEnabled( + FEATURES.SHOW_AI_STUDIO_BANNER_FLAG, +); +const SidebarContextWrapper = ({ + sidebarActor, + isMobile, + children, +}: { + sidebarActor: ActorRef; + isMobile: boolean; + children: ReactNode; +}) => { + const { + isSidebarExpanded, + location, + handleAnnouncementBanner, + openedMenus, + setOpenedMenus, + addMenu, + removeMenu, + toggleSidebar, + isSidebarHidden, + isSearchModalOpen, + handleSearchModal, + isBannerOpen, + } = useSidebarMenu(sidebarActor, isMobile); + + const [isAiStudioBannerDismissed, setIsAiStudioBannerDismissed] = useState( + () => { + return localStorage.getItem("aiStudioBannerDismissed") !== null; + }, + ); + + const showAiStudioBanner = useMemo(() => { + return isAiStudioBannerFlagOn && isPlayground && !isAiStudioBannerDismissed; + }, [isAiStudioBannerDismissed]); + + const memoContextValue = useMemo(() => { + const dismissAiStudioBanner = () => { + localStorage.setItem("aiStudioBannerDismissed", Date.now().toString()); + setIsAiStudioBannerDismissed(true); + }; + + return { + isStateless: false, + open: isSidebarExpanded, + openedMenus, + setOpenedMenus, + addMenu, + removeMenu, + isMobile, + toggleMenu: toggleSidebar, + hideSideBar: isSidebarHidden, + isSearchModalOpen, + setSearchModal: handleSearchModal, + isBannerOpen, + setBannerOpen: handleAnnouncementBanner, + location, + showAiStudioBanner: showAiStudioBanner, + dismissAiStudioBanner, + sidebarActor, + }; + }, [ + isSidebarExpanded, + openedMenus, + setOpenedMenus, + addMenu, + removeMenu, + isMobile, + toggleSidebar, + isSidebarHidden, + isSearchModalOpen, + handleSearchModal, + isBannerOpen, + handleAnnouncementBanner, + location, + showAiStudioBanner, + sidebarActor, + ]); + + return ( + + {children} + + ); +}; + +// Inner component that uses useSelector (only rendered when authService is available) +const SidebarProviderWithAuth = ({ + children, + authService, + isMobile, + defaultContextValue, +}: { + children: ReactNode; + authService: ActorRef; + isMobile: boolean; + defaultContextValue: any; +}) => { + const isAuthenticated = useSelector(authService, getIsAuthenticated); + const noUserManagement = useSelector(authService, getIsNoUserManagement); + + const isSideBarState = useSelector(authService, isSidebarInitialized); + + const sidebarActor = useSelector( + authService, + (state: State) => + state.children["sidebarMachine"], + ); + + const userManagementIsNotAvailable = noUserManagement && isSideBarState; + + const withUserManagement = isAuthenticated && isSideBarState; + + const withSidebarSupport = + isPlayground && authService?.getSnapshot().children["sidebarMachine"]; + + return userManagementIsNotAvailable || + withUserManagement || + withSidebarSupport ? ( + + {children} + + ) : ( + + {children} + + ); +}; + +export const SidebarProvider = ({ children }: SidebarProviderProps) => { + const { authService } = useContext(AuthContext); + + const isMobile = useMediaQuery((theme: Theme) => + theme.breakpoints.down("sm"), + ); + const location = useLocation(); + + // Default context value for when authService is not available or not ready + const defaultContextValue = useMemo(() => { + return { + isStateless: true, + open: true, + openedMenus: ["helpMenu"], + setOpenedMenus: () => {}, + addMenu: () => {}, + removeMenu: () => {}, + isMobile, + toggleMenu: () => {}, + hideSideBar: location.pathname === "/integrations/addIntegration", + isSearchModalOpen: false, + setSearchModal: () => {}, + isBannerOpen: true, + setBannerOpen: () => {}, + dismissAiStudioBanner: () => {}, + }; + }, [isMobile, location.pathname]); + + // If authService is not available, use default context + if (!authService) { + return ( + + {children} + + ); + } + + // authService is available, render the inner component with selectors + return ( + + {children} + + ); +}; diff --git a/ui-next/src/components/Sidebar/createSidebar.ts b/ui-next/src/components/Sidebar/createSidebar.ts new file mode 100644 index 0000000000..44a454fd89 --- /dev/null +++ b/ui-next/src/components/Sidebar/createSidebar.ts @@ -0,0 +1,186 @@ +/** + * Simple sidebar model: items are ordered by id/label; extensions merge via before/after. + * + * - SidebarItem: minimal tree node (id, label, optional children). + * - SidebarMenuExtension: item to insert with optional before/after anchor. + * - createSidebar(base, extensions): returns merged tree with extensions inserted. + */ + +import type { SidebarItemRegistration } from "plugins/registry/types"; +import type { SidebarMenuTarget } from "plugins/registry/types"; + +export type SidebarItem = { + id: string; + label: string; + children?: SidebarItem[]; +}; + +export type SidebarMenuExtension = { + id: string; + label: string; + before?: string; + after?: string; + children?: SidebarMenuExtension[]; +}; + +/** Base OSS sidebar tree (id + label only) used for ordering. Matches core menu structure. */ +export const baseSidebar: SidebarItem[] = [ + { + id: "executionsSubMenu", + label: "Executions", + children: [ + { id: "workflowExeItem", label: "Workflow" }, + { id: "queueMonitorItem", label: "Queue Monitor" }, + ], + }, + { id: "runWorkflow", label: "Run Workflow" }, + { + id: "definitionsSubMenu", + label: "Definitions", + children: [ + { id: "workflowDefItem", label: "Workflow" }, + { id: "taskDefItem", label: "Task" }, + { id: "eventHandlerDefItem", label: "Event Handler" }, + ], + }, + { + id: "helpMenu", + label: "Help", + children: [ + { id: "docsItem", label: "Docs" }, + { id: "requestsItem", label: "Requests" }, + { id: "supportItem", label: "Support" }, + ], + }, + { id: "swaggerItem", label: "API Docs" }, +]; + +/** Collect ids in tree order (depth-first) for a given root. */ +function collectIds(items: SidebarItem[]): string[] { + const ids: string[] = []; + for (const item of items) { + ids.push(item.id); + if (item.children) ids.push(...collectIds(item.children)); + } + return ids; +} + +const rootOrder = collectIds(baseSidebar); +const childOrderByParent = new Map(); +for (const item of baseSidebar) { + if (item.children) { + childOrderByParent.set(item.id, collectIds(item.children)); + } +} + +/** + * Map (targetMenu, position) from plugin API to before/after for createSidebar. + * Uses base sidebar order so position N means "after the (N-1)th item" or "before first" for 0. + */ +function positionToAnchor( + targetMenu: SidebarMenuTarget, + position: "start" | "end" | number | undefined, +): { before?: string; after?: string } { + const order = + targetMenu === "root" + ? rootOrder + : (childOrderByParent.get(targetMenu) ?? []); + const pos = position ?? "end"; + + if (pos === "start" || (typeof pos === "number" && pos <= 0)) { + return order.length ? { before: order[0] } : {}; + } + if (pos === "end") { + return order.length ? { after: order[order.length - 1] } : {}; + } + const index = typeof pos === "number" ? pos : 0; + if (index <= 0) return order.length ? { before: order[0] } : {}; + const afterIndex = Math.min(index - 1, order.length - 1); + return { after: order[afterIndex] }; +} + +/** + * Convert a plugin SidebarItemRegistration to SidebarMenuExtension (before/after). + * Preserves nested items (e.g. adminSubMenu with children). + */ +export function registrationToExtension( + reg: SidebarItemRegistration, +): SidebarMenuExtension { + const anchor = positionToAnchor(reg.targetMenu, reg.position); + return { + id: reg.id, + label: reg.title, + ...anchor, + children: reg.items?.map(registrationToExtension), + }; +} + +/** + * Find the parent array and index of the node with the given id (depth-first search). + * Returns null if not found. + */ +function findInTree( + root: SidebarItem[], + id: string, +): { array: SidebarItem[]; index: number } | null { + for (let i = 0; i < root.length; i++) { + if (root[i].id === id) { + return { array: root, index: i }; + } + if (root[i].children) { + const found = findInTree(root[i].children!, id); + if (found) return found; + } + } + return null; +} + +function extensionToItem(ext: SidebarMenuExtension): SidebarItem { + return { + id: ext.id, + label: ext.label, + children: ext.children?.map(extensionToItem), + }; +} + +function cloneTree(items: SidebarItem[]): SidebarItem[] { + return items.map((item) => ({ + ...item, + children: item.children ? cloneTree(item.children) : undefined, + })); +} + +/** + * Merge extensions into the base sidebar tree using before/after anchors. + * Each extension is inserted after the node with id === ext.after, or before the node with id === ext.before, or appended if neither is set. + */ +export function createSidebar( + base: SidebarItem[], + extensions: SidebarMenuExtension[] = [], +): SidebarItem[] { + const result = cloneTree(base); + + for (const ext of extensions) { + const item = extensionToItem(ext); + + if (ext.after !== undefined) { + const found = findInTree(result, ext.after); + if (found) { + found.array.splice(found.index + 1, 0, item); + } else { + result.push(item); + } + } else if (ext.before !== undefined) { + const found = findInTree(result, ext.before); + if (found) { + found.array.splice(found.index, 0, item); + } else { + result.push(item); + } + } else { + result.push(item); + } + } + + return result; +} diff --git a/ui-next/src/components/Sidebar/hooks/usePendingTasksCount.ts b/ui-next/src/components/Sidebar/hooks/usePendingTasksCount.ts new file mode 100644 index 0000000000..adaabe2fd1 --- /dev/null +++ b/ui-next/src/components/Sidebar/hooks/usePendingTasksCount.ts @@ -0,0 +1,7 @@ +/** + * Returns the number of pending tasks to display as a badge on sidebar items. + * + * In OSS builds this always returns 0. Enterprise plugins supply their own + * badge counts via the `badgeCount` field on SidebarItemRegistration. + */ +export const usePendingTasksCount = (): number => 0; diff --git a/ui-next/src/components/Sidebar/hooks/useSidebarHover.ts b/ui-next/src/components/Sidebar/hooks/useSidebarHover.ts new file mode 100644 index 0000000000..ef870d379f --- /dev/null +++ b/ui-next/src/components/Sidebar/hooks/useSidebarHover.ts @@ -0,0 +1,78 @@ +import { RefObject, useCallback, useEffect, useRef, useState } from "react"; + +export const useSidebarHover = () => { + // Track which menu item is hovered (for showing sub items in popper when collapsed) + const [hoveredMenuId, setHoveredMenuId] = useState(null); + + // Timeout ref for delayed closing + const closeTimeoutRef = useRef | null>(null); + + // Refs for menu items to anchor poppers + const menuItemRefs = useRef>>( + {}, + ); + + const getItemRef = useCallback((itemId: string) => { + if (!menuItemRefs.current[itemId]) { + menuItemRefs.current[itemId] = { current: null }; + } + return menuItemRefs.current[itemId]; + }, []); + + const handleMouseEnter = useCallback((itemId: string) => { + return () => { + // Clear any pending close timeout + if (closeTimeoutRef.current) { + clearTimeout(closeTimeoutRef.current); + closeTimeoutRef.current = null; + } + setHoveredMenuId(itemId); + }; + }, []); + + const handleMouseLeave = useCallback(() => { + // Add a small delay before closing to allow mouse to move to popover + if (closeTimeoutRef.current) { + clearTimeout(closeTimeoutRef.current); + } + closeTimeoutRef.current = setTimeout(() => { + setHoveredMenuId(null); + closeTimeoutRef.current = null; + }, 100); + }, []); + + const handlePopoverMouseEnter = useCallback(() => { + // Clear close timeout when mouse enters popover + if (closeTimeoutRef.current) { + clearTimeout(closeTimeoutRef.current); + closeTimeoutRef.current = null; + } + }, []); + + const handlePopoverMouseLeave = useCallback(() => { + // Close immediately when leaving popover + if (closeTimeoutRef.current) { + clearTimeout(closeTimeoutRef.current); + closeTimeoutRef.current = null; + } + setHoveredMenuId(null); + }, []); + + // Cleanup timeout on unmount + useEffect(() => { + return () => { + if (closeTimeoutRef.current) { + clearTimeout(closeTimeoutRef.current); + } + }; + }, []); + + return { + hoveredMenuId, + getItemRef, + handleMouseEnter, + handleMouseLeave, + handlePopoverMouseEnter, + handlePopoverMouseLeave, + }; +}; diff --git a/ui-next/src/components/Sidebar/index.ts b/ui-next/src/components/Sidebar/index.ts new file mode 100644 index 0000000000..0bd9280a20 --- /dev/null +++ b/ui-next/src/components/Sidebar/index.ts @@ -0,0 +1,4 @@ +export { Sidebar } from "./Sidebar"; +export { SidebarItem } from "./SidebarItem"; +export { SidebarFooter } from "./SidebarFooter"; +export { SubMenu } from "./SubMenu"; diff --git a/ui-next/src/components/Sidebar/sidebarCoreItems.tsx b/ui-next/src/components/Sidebar/sidebarCoreItems.tsx new file mode 100644 index 0000000000..597fa2a402 --- /dev/null +++ b/ui-next/src/components/Sidebar/sidebarCoreItems.tsx @@ -0,0 +1,233 @@ +/** + * Core (OSS) sidebar menu items for Conductor UI. + * + * These items are merged with plugin-registered items in UiSidebar. + * - Executions submenu (Workflow, Queue Monitor) + * - Run Workflow button + * - Definitions submenu (Workflow, Task, Event Handler) + * - Help menu + * - API Docs + */ + +import CodeIcon from "@mui/icons-material/Code"; +import PlayIcon from "@mui/icons-material/PlayArrowOutlined"; +import PlaylistPlayIcon from "@mui/icons-material/PlaylistPlay"; +import SupportIcon from "@mui/icons-material/Support"; +import WebhookOutlinedIcon from "@mui/icons-material/WebhookOutlined"; +import RunWorkflowButton from "components/Sidebar/RunWorkflowButton"; +import { MenuItemType } from "components/Sidebar/types"; +import { FEATURES, featureFlags } from "utils"; +import { + EVENT_HANDLERS_URL, + NEW_TASK_DEF_URL, + RUN_WORKFLOW_URL, + TASK_DEF_URL, + TASK_QUEUE_URL, + WORKFLOW_DEFINITION_URL, + WORKFLOW_EXECUTION_URL, +} from "utils/constants/route"; + +const isPlayground = featureFlags.isEnabled(FEATURES.PLAYGROUND); +const hideFeedbackForm = !featureFlags.isEnabled(FEATURES.SHOW_FEEDBACK_FORM); + +/** + * Core sidebar position constants. Root and submenus both use 100, 200, 300, ... + * so plugins can inject items in between (e.g. position 150 between 100 and 200). + * Export for orkes-conductor-ui to reference when registering sidebar items. + */ +const CORE_SIDEBAR_POSITIONS = { + // Root level (top-level menu items) + ROOT: { + executionsSubMenu: 100, + runWorkflow: 200, + definitionsSubMenu: 300, + helpMenu: 400, + swaggerItem: 500, + }, + // Executions submenu children + EXECUTIONS: { + workflowExeItem: 100, + queueMonitorItem: 200, + }, + // Definitions submenu children + DEFINITIONS: { + workflowDefItem: 100, + taskDefItem: 200, + eventHandlerDefItem: 300, + }, + // Help submenu children + HELP: { + docsItem: 100, + requestsItem: 200, + supportItem: 300, + }, +} as const; + +/** + * Returns the core OSS sidebar menu items. Accepts `open` for the Run Workflow + * button component which depends on sidebar open state. + * Each item has a numeric position so plugins can inject between (e.g. 150 between 100 and 200). + */ +export function getCoreSidebarItems(open: boolean): MenuItemType[] { + const R = CORE_SIDEBAR_POSITIONS.ROOT; + const E = CORE_SIDEBAR_POSITIONS.EXECUTIONS; + const D = CORE_SIDEBAR_POSITIONS.DEFINITIONS; + const H = CORE_SIDEBAR_POSITIONS.HELP; + + return [ + // Executions submenu - core items only + { + id: "executionsSubMenu", + title: "Executions", + icon: , + linkTo: "", + shortcuts: [], + hotkeys: "", + hidden: false, + position: R.executionsSubMenu, + items: [ + { + id: "workflowExeItem", + title: "Workflow", + icon: null, + linkTo: "/executions", + activeRoutes: [WORKFLOW_EXECUTION_URL.WF_ID_TASK_ID], + shortcuts: [], + hotkeys: "", + hidden: false, + position: E.workflowExeItem, + }, + { + id: "queueMonitorItem", + title: "Queue Monitor", + icon: null, + linkTo: TASK_QUEUE_URL.BASE, + shortcuts: [], + hotkeys: "", + hidden: false, + position: E.queueMonitorItem, + }, + ], + }, + // Run Workflow button + { + id: "runWorkflow", + title: "Run Workflow", + icon: , + linkTo: RUN_WORKFLOW_URL, + shortcuts: [], + hidden: true, + position: R.runWorkflow, + component: , + }, + // Definitions submenu - core items only + { + id: "definitionsSubMenu", + title: "Definitions", + icon: , + linkTo: "", + shortcuts: [], + hotkeys: "", + hidden: false, + position: R.definitionsSubMenu, + items: [ + { + id: "workflowDefItem", + title: "Workflow", + icon: null, + linkTo: WORKFLOW_DEFINITION_URL.BASE, + activeRoutes: [ + WORKFLOW_DEFINITION_URL.NEW, + WORKFLOW_DEFINITION_URL.NAME_VERSION, + ], + shortcuts: [], + hotkeys: "", + hidden: false, + position: D.workflowDefItem, + }, + { + id: "taskDefItem", + title: "Task", + icon: null, + linkTo: TASK_DEF_URL.BASE, + activeRoutes: [NEW_TASK_DEF_URL, TASK_DEF_URL.NAME], + shortcuts: [], + hotkeys: "", + hidden: false, + position: D.taskDefItem, + }, + { + id: "eventHandlerDefItem", + title: "Event Handler", + icon: null, + linkTo: EVENT_HANDLERS_URL.BASE, + activeRoutes: [EVENT_HANDLERS_URL.NEW, EVENT_HANDLERS_URL.NAME], + shortcuts: [], + hotkeys: "", + hidden: false, + position: D.eventHandlerDefItem, + }, + ], + }, + // Help menu + { + id: "helpMenu", + title: "Help", + icon: , + linkTo: "", + shortcuts: [], + hotkeys: "", + hidden: false, + position: R.helpMenu, + items: [ + { + id: "docsItem", + title: "Docs", + icon: null, + linkTo: "https://orkes.io/content/", + shortcuts: [], + hotkeys: "", + hidden: false, + position: H.docsItem, + isOpenNewTab: true, + }, + { + id: "requestsItem", + title: "Requests", + icon: null, + linkTo: + "https://orkes.io/orkes-cloud-free-trial?utm_source=playground", + shortcuts: [], + hotkeys: "", + hidden: hideFeedbackForm, + position: H.requestsItem, + isOpenNewTab: true, + }, + { + id: "supportItem", + title: "Support", + icon: null, + linkTo: isPlayground + ? "https://community.orkes.io/ " + : "https://orkeshelp.zendesk.com/hc/en-us/requests/new", + shortcuts: [], + hotkeys: "", + hidden: false, + position: H.supportItem, + isOpenNewTab: true, + }, + ], + }, + // API Docs + { + id: "swaggerItem", + title: "API Docs", + icon: , + linkTo: "/api-reference", + shortcuts: [], + hotkeys: "", + hidden: false, + position: R.swaggerItem, + }, + ]; +} diff --git a/ui-next/src/components/Sidebar/styles.ts b/ui-next/src/components/Sidebar/styles.ts new file mode 100644 index 0000000000..3a31ee42ea --- /dev/null +++ b/ui-next/src/components/Sidebar/styles.ts @@ -0,0 +1,57 @@ +import { colors } from "theme/tokens/variables"; +import { CSSObject } from "@mui/material/styles"; + +export const hoveringStyle: CSSObject = { + color: colors.sidebarBlacky, + backgroundColor: colors.sidebarBarelyPastWhite, +}; + +export const listItemButtonBaseStyle: CSSObject = { + display: "flex", + px: 2.5, + py: 1, + borderRadius: "0px 22px 22px 0px", + transition: "background-color 0.3s ease-in-out", + ":hover": { + zIndex: 1, + }, + ":focus-visible": { + outline: "none", + }, +}; + +export const listItemIconBaseStyle: CSSObject = { + color: "inherit", + minWidth: 0, + justifyContent: "center", + pointerEvents: "none", +}; + +export const listItemTextBaseStyle: CSSObject = { + display: "flex", + alignItems: "center", + "& .MuiListItemText-primary": { + fontStyle: "normal", + fontWeight: 500, + }, +}; + +export const contentBoxBaseStyle: CSSObject = { + display: "flex", + justifyContent: "space-between", + alignItems: "center", + width: "100%", +}; + +export const subItemTextActiveStyle = { + ".MuiListItemText-primary": { + color: colors.sidebarBlacky, + // backgroundColor: colors.sidebarBarelyPastWhite, + borderRadius: "0 22px 22px 0", + height: "100%", + display: "flex", + alignItems: "center", + marginLeft: "-11px", + paddingLeft: "11px", + }, +}; diff --git a/ui-next/src/components/Sidebar/types.ts b/ui-next/src/components/Sidebar/types.ts new file mode 100644 index 0000000000..1848b02fc5 --- /dev/null +++ b/ui-next/src/components/Sidebar/types.ts @@ -0,0 +1,52 @@ +import { ReactNode, ComponentType } from "react"; +import { CSSObject } from "@mui/material/styles"; + +export type MenuItemComponentType = + | ReactNode + | ComponentType<{ + isParent: boolean; + isTopParent: boolean; + isRouteActive: boolean; + isTopParentActive: boolean; + active: boolean; + maybeActiveStyles: CSSObject; + icon?: ReactNode; + }>; + +export interface MenuItemType { + id: string; + title: string; + icon: ReactNode; + linkTo?: string; + activeRoutes?: string[]; + shortcuts: string[]; + hotkeys?: string; + items?: MenuItemType[]; + isSmall?: boolean; + component?: MenuItemComponentType; + hidden: boolean; + isOpenNewTab?: boolean; + textStyle?: CSSObject; + buttonContainerStyle?: CSSObject; + iconContainerStyles?: CSSObject; + handler?: () => void; + /** + * Optional numeric position for ordering (e.g. 10, 20, 30). Gaps allow plugins to + * inject items in between (e.g. position 15 between 10 and 20). + */ + position?: number; + /** + * Optional React hook that returns the current badge count for this item. + * When the returned value is > 0, a red badge with the count is shown next + * to the item title. Enterprise plugins use this to show pending task counts. + * + * Must follow React hook rules (called unconditionally in component render). + * Default: returns 0 (no badge). + */ + useBadgeCount?: () => number; +} + +export interface SubMenuProps extends MenuItemType { + items: MenuItemType[]; + parentId?: string; +} diff --git a/ui-next/src/components/SnackbarMessage.tsx b/ui-next/src/components/SnackbarMessage.tsx new file mode 100644 index 0000000000..70b7718838 --- /dev/null +++ b/ui-next/src/components/SnackbarMessage.tsx @@ -0,0 +1,63 @@ +import { Snackbar, SnackbarOrigin, SxProps } from "@mui/material"; +import MuiAlert from "components/MuiAlert"; +import { WarningCircle } from "@phosphor-icons/react"; +import { ReactNode } from "react"; +// How good is it to use lab components? https://material-ui.com/components/about-the-lab/ + +const useStyles = { + customErrorColor: { + background: "#fdeded", + color: "#622524", + }, + customWarningColor: { + backgroundColor: "#FBA404", + }, +}; + +export const SnackbarMessage = ({ + message, + onDismiss, + severity = "info", + sx = {}, + anchorOrigin = { vertical: "top", horizontal: "center" }, + autoHideDuration = 3000, + id, + action, +}: { + message: string; + onDismiss?: () => void; + severity: "success" | "info" | "warning" | "error"; + sx?: SxProps; + anchorOrigin?: SnackbarOrigin; + autoHideDuration?: number; + id?: string; + action?: ReactNode; +}) => { + const open = !!message; + + return ( + onDismiss && onDismiss()} + open={open} + autoHideDuration={autoHideDuration} + sx={sx} + > + : ""} + variant="filled" + elevation={6} + onClose={() => onDismiss && onDismiss()} + severity={severity} + sx={severity === "error" ? useStyles.customErrorColor : undefined} + id={id} + style={ + severity === "warning" ? useStyles.customWarningColor : undefined + } + action={action} + > + {message} + + + ); +}; diff --git a/ui-next/src/components/SplitButton.jsx b/ui-next/src/components/SplitButton.jsx new file mode 100644 index 0000000000..51224530de --- /dev/null +++ b/ui-next/src/components/SplitButton.jsx @@ -0,0 +1,83 @@ +import React from "react"; +import Grid from "@mui/material/Grid"; +import ButtonGroup from "@mui/material/ButtonGroup"; +import { CaretDown } from "@phosphor-icons/react"; +import ClickAwayListener from "@mui/material/ClickAwayListener"; +import Grow from "@mui/material/Grow"; +import Paper from "@mui/material/Paper"; +import Popper from "@mui/material/Popper"; +import MenuItem from "@mui/material/MenuItem"; +import MenuList from "@mui/material/MenuList"; +import Button from "components/MuiButton"; + +export default function SplitButton({ children, options, onPrimaryClick }) { + const [open, setOpen] = React.useState(false); + const anchorRef = React.useRef(null); + + const handleToggle = () => { + setOpen((prevOpen) => !prevOpen); + }; + + const handleClose = (event) => { + if (anchorRef.current && anchorRef.current.contains(event.target)) { + return; + } + + setOpen(false); + }; + + return ( + + + + + + + + {({ TransitionProps, placement }) => ( + + + + + {options.map(({ label, handler }, index) => ( + { + handler(event, index); + setOpen(false); + }} + > + {label} + + ))} + + + + + )} + + + + ); +} diff --git a/ui-next/src/components/StackTrace.tsx b/ui-next/src/components/StackTrace.tsx new file mode 100644 index 0000000000..1a17edf95b --- /dev/null +++ b/ui-next/src/components/StackTrace.tsx @@ -0,0 +1,41 @@ +import React, { useState } from "react"; + +export function StackTraceComponent({ stacktrace }: { stacktrace: string }) { + const lines = stacktrace.split("\n"); + const head = lines.slice(0, 3); + const tail = lines.slice(3); + + const [collapsed, setCollapsed] = useState(true); + + const toggleCollapsed = () => { + setCollapsed(!collapsed); + }; + + const linkStyle = { + cursor: "pointer", + color: "#1976d2", + }; + + const tailElement = ( + + {tail.join("\n")} +
    +
    + ); + const toggleElement = ( + 3 ? "inherit" : "none", ...linkStyle }} + > + {collapsed ? `${tail.length} more lines` : `Hide ${tail.length} lines`} + + ); + + return ( + + {head.join("\n")} +
    + {tailElement} {toggleElement} +
    + ); +} diff --git a/ui-next/src/components/StatusBadge.tsx b/ui-next/src/components/StatusBadge.tsx new file mode 100644 index 0000000000..891145da2c --- /dev/null +++ b/ui-next/src/components/StatusBadge.tsx @@ -0,0 +1,38 @@ +import { FunctionComponent } from "react"; +import { TaskStatus } from "types/TaskStatus"; +import { HumanTaskState as TaskState } from "types/HumanTaskTypes"; +import { getChipStatusColor } from "utils/helpers"; +import { capitalizeFirstLetter } from "utils/utils"; +import TagChip from "./TagChip"; + +export interface StatusBadgeProps { + status: TaskStatus | TaskState; + labelConcat?: string; +} + +const StatusBadge: FunctionComponent = ({ + status, + labelConcat = "", +}) => { + const color = getChipStatusColor(status); + const chipStyles = + color == null + ? {} + : { + backgroundColor: color, + }; + let formattedStatus = status ? status.toLowerCase() : ""; + formattedStatus = + formattedStatus && formattedStatus.length > 0 + ? capitalizeFirstLetter(formattedStatus) + : ""; + return ( + + ); +}; + +export default StatusBadge; diff --git a/ui-next/src/components/StatusTagChip.tsx b/ui-next/src/components/StatusTagChip.tsx new file mode 100644 index 0000000000..5590170f17 --- /dev/null +++ b/ui-next/src/components/StatusTagChip.tsx @@ -0,0 +1,35 @@ +import CancelOutlinedIcon from "@mui/icons-material/CancelOutlined"; +import { Chip } from "@mui/material"; +import { WorkflowExecutionStatus } from "types/Execution"; +import { TaskStatus } from "types/TaskStatus"; +import { getChipStatusColor } from "utils/helpers"; +import { capitalizeFirstLetter } from "utils/utils"; + +export const renderStatusTagChip = (value: string[], getTagProps: any) => + value.map((val: string | { label: string }, index) => { + const chipBackground = + getChipStatusColor(val as TaskStatus | WorkflowExecutionStatus) || {}; + const renderableLabel: string = + typeof val === "string" || typeof val === "number" ? val : val.label; + + const { key, ...otherTagProps } = getTagProps({ index }); + return ( + } + /> + ); + }); diff --git a/ui-next/src/components/StrikedText.tsx b/ui-next/src/components/StrikedText.tsx new file mode 100644 index 0000000000..afab61e57e --- /dev/null +++ b/ui-next/src/components/StrikedText.tsx @@ -0,0 +1,23 @@ +import { CSSProperties, ReactNode } from "react"; +import MuiTypography from "./MuiTypography"; + +interface StrikedTextProps { + children: ReactNode; + sx?: CSSProperties; +} + +const StrikedText = ({ children, sx, ...props }: StrikedTextProps) => { + const customStyles = { + textDecoration: "line-through", + letterSpacing: "1px", + ...sx, + }; + + return ( + + {children} + + ); +}; + +export default StrikedText; diff --git a/ui-next/src/components/StringArrayFormField.tsx b/ui-next/src/components/StringArrayFormField.tsx new file mode 100644 index 0000000000..f331239a46 --- /dev/null +++ b/ui-next/src/components/StringArrayFormField.tsx @@ -0,0 +1,92 @@ +import { Grid, IconButton } from "@mui/material"; +import { Trash as DeleteIcon, Plus } from "@phosphor-icons/react"; +import { Button, Input } from "components"; +import { ChangeEvent, FunctionComponent, useState } from "react"; +import { adjust, remove } from "utils"; + +interface StringArrayFormFieldProps { + inputParameters: string[]; + onChange: (newInputParams: string[]) => void; + someKey?: string; +} + +export const StringArrayFormField: FunctionComponent< + StringArrayFormFieldProps +> = ({ inputParameters = [], onChange, someKey = "" }) => { + const [newItemValue, setNewItemValue] = useState(""); + const replaceItem = (newValue: string, index: number) => { + onChange(adjust(index, () => newValue, inputParameters)); + }; + + const deleteItem = (idx: number) => { + onChange(remove(idx, 1, inputParameters)); + }; + const addItem = () => { + onChange(inputParameters.concat(newItemValue)); + setNewItemValue(""); + }; + + const handleFocus = ( + e: ChangeEvent, + ) => { + e.target.select(); + }; + + return ( + <> + {inputParameters.map((value, index) => ( + + + { + replaceItem(newValue, index); + }} + value={value} + autoFocus + onFocus={( + e: ChangeEvent, + ) => handleFocus(e)} + placeholder="e.g.: Cache-Control..." + sx={{ minWidth: "150px" }} + /> + + + + deleteItem(index)}> + + + + + ))} + + + + + + + ); +}; diff --git a/ui-next/src/components/SubjectSelector/SubjectMultiPicker.tsx b/ui-next/src/components/SubjectSelector/SubjectMultiPicker.tsx new file mode 100644 index 0000000000..8d0feee3fc --- /dev/null +++ b/ui-next/src/components/SubjectSelector/SubjectMultiPicker.tsx @@ -0,0 +1,106 @@ +import { Autocomplete, ListItem, ListItemText, Popper } from "@mui/material"; +import { + AppWindow as ApplicationIcon, + UsersThree as GroupIcon, + User as UserIcon, +} from "@phosphor-icons/react"; +import TagChip from "components/TagChip"; +import ConductorInput from "components/v1/ConductorInput"; +import XCloseIcon from "components/v1/icons/XCloseIcon"; +import { CSSProperties, FunctionComponent } from "react"; +import { autocompleteStyle } from "shared/styles"; +import { SelectableOption } from "./types"; + +interface SubjectMultiPickerProps { + multiple: boolean; + options: SelectableOption[]; + onChange: (val: SelectableOption | SelectableOption[]) => void; + label: string; + value?: any; + required?: boolean; + growPopper?: boolean; +} + +const ICON_SIZE = 16; + +export const SubjectMultiPicker: FunctionComponent = ({ + multiple, + options, + onChange, + label, + value, + required = false, + growPopper, +}) => { + const popperStyle = (style: CSSProperties | undefined) => { + return growPopper ? { maxWidth: "300px" } : style; + }; + return ( + ( + + )} + value={value} + isOptionEqualToValue={(option, value) => option.id === value.id} + multiple={multiple} + options={options as SelectableOption[]} + getOptionLabel={(option: any) => option?.display || ""} + freeSolo + renderTags={(value, getTagProps) => + value.map((option: SelectableOption, index) => { + const { key, ...otherTagProps } = getTagProps({ index }); + return ( + + ) : option.type === "group" ? ( + + ) : ( + + ) + } + label={option.display} + {...otherTagProps} + /> + ); + }) + } + filterSelectedOptions + onChange={(_event, newValue: any) => { + if (newValue !== value) { + onChange(newValue as SelectableOption | SelectableOption[]); + } + }} + onInputChange={(_event, newInputValue, reason) => { + // Only handle user input, not programmatic changes + if (reason === "input") { + const newOption = { + value: newInputValue, + id: newInputValue, + display: newInputValue, + } as SelectableOption; + if (newOption.value !== value?.value) { + onChange(newOption); + } + } + }} + renderOption={(props, option) => ( + + + + )} + renderInput={(params) => ( + + )} + sx={[autocompleteStyle({ value })]} + clearIcon={} + /> + ); +}; diff --git a/ui-next/src/components/SubjectSelector/SubjectSelector.tsx b/ui-next/src/components/SubjectSelector/SubjectSelector.tsx new file mode 100644 index 0000000000..17ab339510 --- /dev/null +++ b/ui-next/src/components/SubjectSelector/SubjectSelector.tsx @@ -0,0 +1,146 @@ +import { FunctionComponent, useMemo } from "react"; +import { SubjectMultiPicker } from "./SubjectMultiPicker"; +import { SelectableOption, SelectableOptionType } from "./types"; +import { AccessGroup, User } from "types"; +import { Application } from "types/Application"; +import { displayUserSubject } from "./helpers"; + +type SubjectSelectorBaseParentProps = { + label?: string; + selectableUsers: User[]; + selectableGroups: AccessGroup[]; + selectableApplications: Application[]; + growPopper?: boolean; +}; + +type SubjectSelectorMultipleBaseProps = SubjectSelectorBaseParentProps & { + multiple: true; + onChange: (value: SelectableOption | SelectableOption[]) => void; + selectedSubjectsValue: string[]; +}; + +type SubjectSelectorSingleBaseProps = SubjectSelectorBaseParentProps & { + multiple: false; + onChange: (value: SelectableOption | SelectableOption[]) => void; + selectedSubjectsValue?: string; +}; + +export const SubjectSelectorBase: FunctionComponent< + SubjectSelectorMultipleBaseProps | SubjectSelectorSingleBaseProps +> = ({ + label, + selectableUsers, + selectableGroups, + selectableApplications, + onChange, + selectedSubjectsValue, + multiple, + growPopper, +}) => { + const options = useMemo((): SelectableOption[] => { + return selectableUsers + .map( + (user: User): SelectableOption => ({ + display: displayUserSubject(user), + id: user.id, + value: `${user.id}`, + type: SelectableOptionType.USER, + }), + ) + .concat( + selectableGroups.map( + (group: AccessGroup): SelectableOption => ({ + display: group.id, + id: group.id, + value: `${group.id}`, + type: SelectableOptionType.GROUP, + }), + ), + ) + .concat( + selectableApplications.map( + (application: Application): SelectableOption => ({ + display: application.name, + id: application.id, + value: `USER:app:${application.id}`, + type: SelectableOptionType.APPLICATION, + }), + ), + ); + }, [selectableUsers, selectableGroups, selectableApplications]); + + const value = useMemo((): SelectableOption[] | SelectableOption => { + if (multiple === false) { + const foundElement = options.find( + ({ value }) => value === selectedSubjectsValue, + ); + if (foundElement) { + return foundElement; + } + // Support for free solo + return { + value: selectedSubjectsValue, + id: selectedSubjectsValue, + display: selectedSubjectsValue, + } as SelectableOption; + } + + const [users, groups, applications] = Array.isArray(selectedSubjectsValue) + ? selectedSubjectsValue.reduce( + ( + acc: [string[], string[], string[]], + c: string, + ): [string[], string[], string[]] => { + const [accUsers, accGroups, accApplications] = acc; + if (c.includes("USER:app:")) { + return [ + accUsers, + accGroups, + accApplications.concat(c.replace(/^USER:app:/, "")), + ]; + } + if (c.includes("CONDUCTOR_USER:")) { + return [ + accUsers.concat(c.replace(/^CONDUCTOR_USER:/, "")), + accGroups, + accApplications, + ]; + } + if (c.includes("CONDUCTOR_GROUP:")) { + return [ + accUsers, + accGroups.concat(c.replace(/^CONDUCTOR_GROUP:/, "")), + accApplications, + ]; + } + return acc; + }, + [[], [], []], + ) + : [[], [], []]; + + return options.filter(({ id, type }) => { + if (type === SelectableOptionType.USER) { + return users.includes(id); + } + if (type === SelectableOptionType.GROUP) { + return groups.includes(id); + } + if (type === SelectableOptionType.APPLICATION) { + return applications.includes(id); + } + throw new Error("Unexpected type: ", type); + }); + }, [options, selectedSubjectsValue, multiple]); + + return ( + + ); +}; diff --git a/ui-next/src/components/SubjectSelector/helpers.ts b/ui-next/src/components/SubjectSelector/helpers.ts new file mode 100644 index 0000000000..1d61e4540e --- /dev/null +++ b/ui-next/src/components/SubjectSelector/helpers.ts @@ -0,0 +1,4 @@ +import { User } from "types"; + +export const displayUserSubject = (user: User): string => + `${user.id} (${user.name})`; diff --git a/ui-next/src/components/SubjectSelector/index.ts b/ui-next/src/components/SubjectSelector/index.ts new file mode 100644 index 0000000000..e69310635e --- /dev/null +++ b/ui-next/src/components/SubjectSelector/index.ts @@ -0,0 +1,4 @@ +export * from "./SubjectSelector"; +export * from "./SubjectMultiPicker"; +export * from "./types"; +export * from "./helpers"; diff --git a/ui-next/src/components/SubjectSelector/types.ts b/ui-next/src/components/SubjectSelector/types.ts new file mode 100644 index 0000000000..21a0daec6f --- /dev/null +++ b/ui-next/src/components/SubjectSelector/types.ts @@ -0,0 +1,12 @@ +export enum SelectableOptionType { + USER = "user", + GROUP = "group", + APPLICATION = "application", +} + +export type SelectableOption = { + display: string; + id: string; + value: string; + type: SelectableOptionType; +}; diff --git a/ui-next/src/components/SubmitFormWrapper.jsx b/ui-next/src/components/SubmitFormWrapper.jsx new file mode 100644 index 0000000000..d2f5f7b1c4 --- /dev/null +++ b/ui-next/src/components/SubmitFormWrapper.jsx @@ -0,0 +1,13 @@ +export default function SubmitFormWrapper({ onSubmit, children }) { + return ( +
    { + onSubmit(); + event.preventDefault(); + return false; + }} + > + {children} +
    + ); +} diff --git a/ui-next/src/components/Tabs.jsx b/ui-next/src/components/Tabs.jsx new file mode 100644 index 0000000000..748e7cd6db --- /dev/null +++ b/ui-next/src/components/Tabs.jsx @@ -0,0 +1,74 @@ +import React from "react"; +import { Tab as RawTab, Tabs as RawTabs } from "@mui/material"; +import { colors } from "../theme/tokens/variables"; +import { getTheme } from "../theme"; + +// Override styles for 'Contextual' tabs +const contextualTabStyle = { + root: { + color: colors.gray02, + textTransform: "none", + height: "38px", + minHeight: "38px", + padding: "12px 16px", + backgroundColor: colors.gray13, + [getTheme().breakpoints.up("md")]: { + minWidth: 0, + }, + width: "auto", + "&:hover": { + backgroundColor: colors.grayXLight, + color: colors.gray02, + }, + }, + selected: { + backgroundColor: "white", + color: colors.black, + "&:hover": { + backgroundColor: "white", + color: colors.black, + }, + }, + wrapper: { + width: "auto", + }, +}; + +const regularTabStyle = { + root: { + "& .MuiTab-root": { + minWidth: "130px", + fontWeight: "normal", + fontSize: "14px", + }, + }, +}; + +const contextualTabsStyle = { + indicator: { + height: 0, + }, + flexContainer: { + backgroundColor: colors.gray13, + }, +}; + +export default function Tabs({ contextual, children, ...props }) { + return ( + + {contextual + ? children.map((child, idx) => + React.cloneElement(child, { contextual: true, key: idx }), + ) + : children} + + ); +} + +export function Tab({ contextual = null, ...props }) { + return ; +} diff --git a/ui-next/src/components/TagChip.tsx b/ui-next/src/components/TagChip.tsx new file mode 100644 index 0000000000..857d6c6a7c --- /dev/null +++ b/ui-next/src/components/TagChip.tsx @@ -0,0 +1,20 @@ +import { Chip, ChipProps } from "@mui/material"; +import { forwardRef } from "react"; +import { colors } from "theme/tokens/variables"; + +const customStyles = { + background: colors.otherTag, + color: "black", + fontWeight: 400, + borderRadius: "100px", + fontSize: "12px", +}; + +const TagChip = forwardRef( + ({ style = {}, ...props }, ref) => { + const combinedStyles = { ...customStyles, ...style }; + return ; + }, +); + +export default TagChip; diff --git a/ui-next/src/components/Text.jsx b/ui-next/src/components/Text.jsx new file mode 100644 index 0000000000..f9706de7c4 --- /dev/null +++ b/ui-next/src/components/Text.jsx @@ -0,0 +1,9 @@ +import MuiTypography from "./MuiTypography"; + +const levelMap = ["caption", "body2", "body1"]; + +const Text = ({ level = 1, sx, ...props }) => { + return ; +}; + +export default Text; diff --git a/ui-next/src/components/TwoPanesDivider.jsx b/ui-next/src/components/TwoPanesDivider.jsx new file mode 100644 index 0000000000..7c6693d2d1 --- /dev/null +++ b/ui-next/src/components/TwoPanesDivider.jsx @@ -0,0 +1,193 @@ +import { useCallback, useRef, useState } from "react"; +import { createTheme, useTheme, ThemeProvider } from "@mui/material"; +import { colors } from "theme/tokens/variables"; + +import getTheme from "theme/theme"; +import { Box } from "@mui/material"; +import useMediaQuery from "@mui/material/useMediaQuery"; + +const MIN_LEFT_WIDTH = 400; +const MIN_RIGHT_WIDTH = 150; +const SMALL_PERCENT_THREASHOLD = 34; + +const smallThemeCreate = ( + _existingTheme, // ignore existingTheme for now. since it will make font bigger when not on mobile +) => + createTheme({ + ...getTheme(), + breakpoints: { + values: { + xs: 0, + sm: 20, + }, + }, + }); + +const TwoPanesBoxider = ({ + leftPanelContent, + rightPanelContent, + leftPanelExpanded = false, + setLeftPanelExpanded, +}) => { + const theme = useTheme(); + // Checking responsive width + const isValidWidth = useMediaQuery((theme) => theme.breakpoints.down("sm")); + + const [isHoveringResizer, setIsHoveringResizer] = useState(false); + const [rightPanelTheme, setRightPanelTheme] = useState({ + theme, + name: "default", + }); + + const containerRef = useRef(null); + const leftPanelRef = useRef(null); + const rightPanelRef = useRef(null); + const resizerRef = useRef(null); + + const handleMouseDown = () => { + document.addEventListener("mouseup", handleMouseUp, true); + document.addEventListener("mousemove", handleMouseMove, true); + }; + + const handleMouseUp = () => { + document.removeEventListener("mouseup", handleMouseUp, true); + document.removeEventListener("mousemove", handleMouseMove, true); + }; + + const handleMouseMove = useCallback( + (e) => { + e.preventDefault(); + + const boundingClientRect = leftPanelRef.current.getBoundingClientRect(); + + const leftWidth = e.clientX - boundingClientRect.x; + const containerWidth = containerRef.current.offsetWidth; + const rightWidth = containerWidth - leftWidth; + + const leftWidthAsPercent = (leftWidth / containerWidth) * 100; + const rightWidthAsPercent = (rightWidth / containerWidth) * 100; + + if (leftWidth >= MIN_LEFT_WIDTH && rightWidth >= MIN_RIGHT_WIDTH) { + leftPanelRef.current.style.width = `${leftWidthAsPercent}%`; + rightPanelRef.current.style.width = `${rightWidthAsPercent}%`; + resizerRef.current.style.left = `calc(${leftWidthAsPercent}% - 3px)`; + } + + const isNotMobileAndRightPanelIsSmall = + !isValidWidth && SMALL_PERCENT_THREASHOLD > rightWidthAsPercent; + + if (isNotMobileAndRightPanelIsSmall) { + setRightPanelTheme({ theme: smallThemeCreate(theme), name: "small" }); + } else { + setRightPanelTheme({ theme, name: "default" }); + } + }, + [theme, isValidWidth], + ); + + return ( + + + setLeftPanelExpanded(!leftPanelExpanded)} + sx={{ + display: [leftPanelExpanded ? "none" : "block", "none"], + width: "100%", + height: "100%", + background: "black", + opacity: 0.4, + position: "absolute", + top: 0, + left: 0, + zIndex: 999, + }} + > + + {leftPanelContent} + + + + + + + {rightPanelContent} + + + + + + handleMouseDown(e)} + onMouseEnter={(_e) => setIsHoveringResizer(true)} + onMouseLeave={(_e) => setIsHoveringResizer(false)} + id="editor-panel-resize-line" + sx={{ + position: "absolute", + left: "50%", + height: "100%", + width: "8px", + marginLeft: "-4px", + cursor: "col-resize", + backgroundColor: colors.primary, + opacity: isHoveringResizer ? 1 : 0, + transition: "opacity 0.10s ease-in-out", + zIndex: 5, + flexShrink: 0, + resize: "horizontal", + display: ["none", leftPanelExpanded ? "none" : "block"], + }} + /> + + ); +}; + +export default TwoPanesBoxider; diff --git a/ui-next/src/components/UIModal.tsx b/ui-next/src/components/UIModal.tsx new file mode 100644 index 0000000000..4f1b42d2af --- /dev/null +++ b/ui-next/src/components/UIModal.tsx @@ -0,0 +1,224 @@ +import { Box, DialogActions, SxProps, Theme } from "@mui/material"; +import Dialog, { DialogProps } from "@mui/material/Dialog"; +import { XCircle } from "@phosphor-icons/react"; +import React, { CSSProperties, ReactNode, forwardRef, useState } from "react"; +import { + defaultModalBackdropColor, + seGrey2, + blueLight, +} from "theme/tokens/colors"; + +type UIModalProps = Omit & { + style?: CSSProperties; + setOpen: (value: boolean) => void; + title?: string | React.ReactNode; + description?: string | React.ReactNode; + icon?: React.ReactNode; + enableCloseButton?: boolean; + backdropColor?: string; + maxWidth?: any; + footerChildren?: ReactNode; + footerSx?: SxProps; + titleSx?: SxProps; +}; + +const modalStyles = { + background: "#FFFFFF", + boxShadow: "4px 4px 10px 0px rgba(89, 89, 89, 0.41)", + p: 4, + overflow: "auto", +}; + +const headerStyles = { + display: "flex", + alignItems: "flex-start", + "& > *": { + padding: "3px", + "&:first-of-type": { + paddingLeft: "0px", + }, + }, +}; + +const titleStyles: SxProps = { + fontSize: "16px", + lineHeight: "16px", + fontWeight: 600, + textTransform: "uppercase", +}; +const descStyles = { + color: "#858585", + fontSize: "12px", + fontWeight: 300, + lineHeight: "18px", + paddingTop: "2px", + display: "-webkit-box", + WebkitLineClamp: 2, + WebkitBoxOrient: "vertical", + overflow: "hidden", + textOverflow: "ellipsis", + cursor: "pointer", +}; +const contentStyles = { + padding: "15px 22px 20px 25px", +}; + +const TruncatedDescription = ({ + description, +}: { + description: string | ReactNode; +}) => { + const [isExpanded, setIsExpanded] = useState(false); + + if (typeof description !== "string") { + return {description}; + } + + const maxLength = 330; + + const truncatedText = + !isExpanded && description.length > maxLength + ? description.substring(0, maxLength) + "... " + : description; + + return ( + setIsExpanded(!isExpanded)} + > + {truncatedText} + {!isExpanded && description.length > maxLength && ( + { + e.stopPropagation(); + setIsExpanded(true); + }} + > + Read more + + )} + + ); +}; + +const UIModal = forwardRef( + ( + { + style, + open, + setOpen, + children, + icon, + title, + description, + enableCloseButton, + maxWidth, + backdropColor, + footerChildren, + footerSx, + id, + titleSx, + ...props + }, + ref, + ) => { + const backdropStyles = { + background: backdropColor ? backdropColor : defaultModalBackdropColor, + opacity: "0.75 !important", + }; + return ( + setOpen(false)} + PaperProps={{ + style: { borderRadius: "6px" }, + ...(props.PaperProps !== undefined ? props.PaperProps : {}), + }} + slotProps={{ + backdrop: { + sx: { ...backdropStyles }, + }, + }} + aria-labelledby={`alert-dialog-${title}`} + > + + + {icon && ( + *": { + fontSize: "20px", + }, + }} + > + {icon} + + )} + + {title && ( + + {title} + + )} + {description && ( + + )} + + {enableCloseButton && ( + setOpen(false)} + > + + + )} + + {children} + + {footerChildren && ( + {footerChildren} + )} + + ); + }, +); + +export default UIModal; +export type { UIModalProps }; diff --git a/ui-next/src/components/WorkflowStatusBadge.tsx b/ui-next/src/components/WorkflowStatusBadge.tsx new file mode 100644 index 0000000000..b71bb941a8 --- /dev/null +++ b/ui-next/src/components/WorkflowStatusBadge.tsx @@ -0,0 +1,35 @@ +import { FunctionComponent } from "react"; +import { getChipStatusColor } from "utils/helpers"; +import { capitalizeFirstLetter } from "utils/utils"; +import TagChip from "./TagChip"; +import { WorkflowExecutionStatus } from "types/Execution"; + +export interface WorkflowStatusBadgeProps { + status: WorkflowExecutionStatus; +} + +const WorkflowStatusBadge: FunctionComponent = ({ + status, +}) => { + const color = getChipStatusColor(status); + const chipStyles = + color == null + ? {} + : { + backgroundColor: color, + }; + let formattedStatus = status ? status.toLowerCase() : ""; + formattedStatus = + formattedStatus && formattedStatus.length > 0 + ? capitalizeFirstLetter(formattedStatus) + : ""; + return ( + + ); +}; + +export default WorkflowStatusBadge; diff --git a/ui-next/src/components/agent/Agent.tsx b/ui-next/src/components/agent/Agent.tsx new file mode 100644 index 0000000000..84cd125f9b --- /dev/null +++ b/ui-next/src/components/agent/Agent.tsx @@ -0,0 +1,6 @@ +// OSS stub — the full Agent component lives in enterprise/components/agent/Agent +// In OSS builds this renders nothing; enterprise wires up the real component +// via the AgentLayout plugin. +export default function Agent(_props: Record) { + return null; +} diff --git a/ui-next/src/components/agent/AgentContext.tsx b/ui-next/src/components/agent/AgentContext.tsx new file mode 100644 index 0000000000..6174d3db10 --- /dev/null +++ b/ui-next/src/components/agent/AgentContext.tsx @@ -0,0 +1,66 @@ +import { createContext, useContext, ReactNode, useMemo } from "react"; + +type AgentContextType = { + sendMessage: (message: string) => void; + applySuggestion: ( + messageId: string, + accepted: boolean, + feedback?: string, + ) => void; + clearMessages: () => void; + cancelStream: () => void; + resumeStream?: () => void; +}; + +const AgentContext = createContext(null); + +export function AgentProvider({ + children, + sendMessage, + applySuggestion, + clearMessages, + cancelStream, + resumeStream, +}: { + children: ReactNode; + sendMessage: (message: string) => void; + applySuggestion: ( + messageId: string, + accepted: boolean, + feedback?: string, + ) => void; + clearMessages: () => void; + cancelStream: () => void; + resumeStream?: () => void; +}) { + const value = useMemo( + () => ({ + sendMessage, + applySuggestion, + clearMessages, + cancelStream, + resumeStream, + }), + [sendMessage, applySuggestion, clearMessages, cancelStream, resumeStream], + ); + + return ( + {children} + ); +} + +// eslint-disable-next-line react-refresh/only-export-components +export function useAgentContext() { + const context = useContext(AgentContext); + if (!context) { + // Return no-op functions if not in provider context + return { + sendMessage: () => {}, + applySuggestion: () => {}, + clearMessages: () => {}, + cancelStream: () => {}, + resumeStream: undefined, + }; + } + return context; +} diff --git a/ui-next/src/components/agent/AgentEditorController.tsx b/ui-next/src/components/agent/AgentEditorController.tsx new file mode 100644 index 0000000000..c95b2c5810 --- /dev/null +++ b/ui-next/src/components/agent/AgentEditorController.tsx @@ -0,0 +1,4 @@ +// OSS stub — the full AgentEditorController lives in enterprise/components/agent/ +export function AgentEditorController(_props: Record) { + return null; +} diff --git a/ui-next/src/components/agent/agent-types.ts b/ui-next/src/components/agent/agent-types.ts new file mode 100644 index 0000000000..d19176eb43 --- /dev/null +++ b/ui-next/src/components/agent/agent-types.ts @@ -0,0 +1,26 @@ +export enum AgentDisplayMode { + FLOATING_EXPANDED = "floating-expanded", + FLOATING_MINIMIZED = "floating-minimized", + TABBED = "tabbed", + CLOSED = "closed", + FULL_PAGE = "full-page", + RIGHT_SIDEBAR = "right-sidebar", +} + +export enum AgentContentTab { + CHAT = "chat", + CONVERSATIONS = "conversations", +} +export interface Message { + role: "user" | "assistant"; + content: string; +} +export interface Conversation { + sessionId: string; + title: string; + messageCount: number; + workflowName?: string; + createdAt: string; + updatedAt: string; + status: string; +} diff --git a/ui-next/src/components/agent/helpers.ts b/ui-next/src/components/agent/helpers.ts new file mode 100644 index 0000000000..0be3be0000 --- /dev/null +++ b/ui-next/src/components/agent/helpers.ts @@ -0,0 +1,7 @@ +export const testWorkflowDefOrExecutionViewPathname = (pathname: string) => { + return ( + /^\/workflowDef\/.*$/.test(pathname) || + /^\/execution\/.*$/.test(pathname) || + pathname.startsWith("/newWorkflowDef") + ); +}; diff --git a/ui-next/src/components/auth/AuthGuard.tsx b/ui-next/src/components/auth/AuthGuard.tsx new file mode 100644 index 0000000000..f48b080827 --- /dev/null +++ b/ui-next/src/components/auth/AuthGuard.tsx @@ -0,0 +1,59 @@ +/** + * Layout wrapper for OSS mode. + * + * In OSS mode, this is simply a layout container with no authentication checks. + * Full auth guard logic has been moved to the enterprise package. + */ +import { Box } from "@mui/material"; +import { RunWorkflow } from "pages/runWorkflow"; +import React from "react"; +import { Outlet } from "react-router"; +import ErrorBoundary from "../ErrorBoundary"; + +interface AuthGuardProps { + fallback?: React.ReactNode; + runWorkflow?: boolean; +} + +const AuthGuard = ({ + fallback: _fallback = null, + runWorkflow = false, +}: AuthGuardProps) => { + return ( + + {runWorkflow && } + + + + + + + ); +}; + +export default AuthGuard; diff --git a/ui-next/src/components/charts/CacheChart.tsx b/ui-next/src/components/charts/CacheChart.tsx new file mode 100644 index 0000000000..e7fc5c1fef --- /dev/null +++ b/ui-next/src/components/charts/CacheChart.tsx @@ -0,0 +1,80 @@ +import { + Area, + AreaChart, + CartesianGrid, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; +import { + BaseChartProps, + formatHistoricalData, + formatXAxis, + getTimeTicks, + useChartColors, +} from "./chartUtils"; + +export function CacheChart({ historicalData = [] }: BaseChartProps) { + const colors = useChartColors(); + const data = formatHistoricalData(historicalData); + const xTicks = getTimeTicks(data); + + return ( + + + + + `${(value * 100).toFixed(0)}%`} + stroke={colors.text} + width={60} + /> + `${(Number(value) * 100).toFixed(2)}%`} + contentStyle={{ + backgroundColor: colors.isDark ? "#1f2937" : "#fff", + borderColor: colors.grid, + }} + /> + + + + + ); +} diff --git a/ui-next/src/components/charts/ErrorsChart.tsx b/ui-next/src/components/charts/ErrorsChart.tsx new file mode 100644 index 0000000000..04b4bf460b --- /dev/null +++ b/ui-next/src/components/charts/ErrorsChart.tsx @@ -0,0 +1,154 @@ +import { Box, Paper, Stack, Typography } from "@mui/material"; +import _mergeWith from "lodash/mergeWith"; +import _sum from "lodash/sum"; +import { getHttpStatusText } from "utils/httpStatus"; +import { + Area, + AreaChart, + CartesianGrid, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; +import { + BaseChartProps, + formatHistoricalData, + formatXAxis, + getTimeTicks, + useChartColors, +} from "./chartUtils"; + +export function ErrorsChart({ historicalData = [] }: BaseChartProps) { + const colors = useChartColors(); + const data = formatHistoricalData(historicalData); + const xTicks = getTimeTicks(data); + + const errorBreakdown: Record = _mergeWith( + {}, + ...data.map((point) => point.errorsByStatusCode || {}), + (objValue: number, srcValue: number) => (objValue || 0) + (srcValue || 0), + ); + + const totalErrors = _sum(Object.values(errorBreakdown)); + + return ( + + + + + + `${(value * 100).toFixed(1)}%`} + stroke={colors.text} + width={60} + /> + `${(Number(value) * 100).toFixed(2)}%`} + contentStyle={{ + backgroundColor: colors.isDark ? "#1f2937" : "#fff", + borderColor: colors.grid, + }} + /> + + + + + {totalErrors > 0 && ( + + + Error Breakdown + + + Types of errors encountered + + + {Object.entries(errorBreakdown).map(([statusCode, count]) => { + const percentage = ((count as number) / totalErrors) * 100; + const getStatusColor = (code: string) => { + if (code.startsWith("5")) return colors.error; + if (code.startsWith("4")) return colors.tertiary; + return colors.secondary; + }; + + return ( + + + + {statusCode} {getHttpStatusText(statusCode)} + + + {count} occurrences ({percentage.toFixed(1)}%) + + + + + + + ); + })} + + + )} + + ); +} diff --git a/ui-next/src/components/charts/LatencyChart.tsx b/ui-next/src/components/charts/LatencyChart.tsx new file mode 100644 index 0000000000..b840d85680 --- /dev/null +++ b/ui-next/src/components/charts/LatencyChart.tsx @@ -0,0 +1,110 @@ +import { + CartesianGrid, + Legend, + Line, + LineChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; +import type { ValueType } from "recharts/types/component/DefaultTooltipContent"; +import { + LatencyChartProps, + formatHistoricalData, + formatXAxis, + getTimeTicks, + useChartColors, +} from "./chartUtils"; + +export function LatencyChart({ + historicalData = [], + visiblePercentiles = { p50: true, p95: true, p99: true }, +}: LatencyChartProps) { + const colors = useChartColors(); + const data = formatHistoricalData(historicalData); + const xTicks = getTimeTicks(data); + + return ( + + + + + (v != null ? `${v} ms` : "")} + width={68} + label={{ + value: "Latency (ms)", + angle: -90, + position: "insideLeft", + fill: colors.text, + dx: -8, + dy: 0, + style: { fontSize: 13, fontWeight: 500 }, + }} + /> + [ + `${typeof value === "number" ? value.toFixed(2) : value} ms`, + name, + ]} + /> + + {visiblePercentiles.p50 && ( + + )} + {visiblePercentiles.p95 && ( + + )} + {visiblePercentiles.p99 && ( + + )} + + + ); +} diff --git a/ui-next/src/components/charts/RequestsChart.tsx b/ui-next/src/components/charts/RequestsChart.tsx new file mode 100644 index 0000000000..0f31440a76 --- /dev/null +++ b/ui-next/src/components/charts/RequestsChart.tsx @@ -0,0 +1,66 @@ +import { + Area, + AreaChart, + CartesianGrid, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; +import { + BaseChartProps, + formatHistoricalData, + formatXAxis, + getTimeTicks, + useChartColors, +} from "./chartUtils"; + +export function RequestsChart({ historicalData = [] }: BaseChartProps) { + const colors = useChartColors(); + const data = formatHistoricalData(historicalData); + const xTicks = getTimeTicks(data); + + return ( + + + + + + + + + + ); +} diff --git a/ui-next/src/components/charts/chartUtils.ts b/ui-next/src/components/charts/chartUtils.ts new file mode 100644 index 0000000000..5823e2afbc --- /dev/null +++ b/ui-next/src/components/charts/chartUtils.ts @@ -0,0 +1,137 @@ +import { useTheme } from "@mui/material"; +import { FormattedHistoricalData, HistoricalData } from "types/MetricsTypes"; + +export enum ChartType { + REQUESTS = "requests", + LATENCY = "latency", + ERRORS = "errors", + CACHE = "cache", +} + +export enum ThemeMode { + DARK = "dark", + LIGHT = "light", +} + +export interface BaseChartProps { + historicalData?: HistoricalData[]; +} + +export interface LatencyChartProps extends BaseChartProps { + visiblePercentiles?: Record; +} + +export function formatHistoricalData(data: HistoricalData[] = []) { + const round2 = (x: number) => + typeof x === "number" ? Math.round(x * 100) / 100 : x; + + return data.map((d) => { + const dateObj = + d.time != null + ? typeof d.time === "number" + ? new Date(d.time * (d.time > 1e12 ? 1 : 1000)) + : new Date(d.time) + : null; + + // Calculate error rate + const errorRate = d.requestCount > 0 ? d.errorCount / d.requestCount : 0; + + return { + ...d, + time: dateObj, + requests: d.requestCount ?? 0, + errors: errorRate, + p50: round2(d.p50), + p95: round2(d.p95), + p99: round2(d.p99), + errorsByStatusCode: d.errorsByStatusCode || {}, + }; + }); +} + +// Smart x-axis label: always show HH:mm for time ticks, and show date/year only for the very first tick if needed +export const formatXAxis = ( + tickItem: Date | string | number, + index: number, +) => { + if (tickItem == null) return ""; + let d: Date | null = null; + try { + if (typeof tickItem === "number") { + // Handle millisecond timestamps + d = new Date(tickItem); + } else if (typeof tickItem === "string") { + d = new Date(tickItem); + } else { + d = tickItem; + } + + if (!d || !(d instanceof Date) || isNaN(d.getTime())) { + console.warn("Invalid date value:", tickItem); + return ""; + } + + // Format time as HH:mm + const hours = d.getHours().toString().padStart(2, "0"); + const minutes = d.getMinutes().toString().padStart(2, "0"); + const timeLabel = `${hours}:${minutes}`; + + // For the first tick, show date and time + if (index === 0) { + const day = d.getDate().toString().padStart(2, "0"); + const month = (d.getMonth() + 1).toString().padStart(2, "0"); + const year = d.getFullYear(); + const currentYear = new Date().getFullYear(); + + const dateLabel = + year !== currentYear ? `${day}/${month}/${year}` : `${day}/${month}`; + + return `${dateLabel} ${timeLabel}`; + } + + return timeLabel; + } catch (error) { + console.error("Error formatting x-axis label:", error); + return ""; + } +}; + +// Helper to generate at least 20 ticks for X axis, evenly spaced, covering the full time range +export const getTimeTicks = (data: FormattedHistoricalData[]) => { + if (!data || data.length === 0) return []; + const times = data + .filter((d) => d.time !== null) + .map((d) => { + const time = d.time instanceof Date ? d.time : new Date(d.time!); + return time.getTime(); // Ensure we're working with timestamps + }); + if (times.length === 0) return []; + const min = times[0]; + const max = times[times.length - 1]; + const desiredTicks = 20; + if (max === min) return [min]; // edge case: all data at one point + const intervalMs = Math.max(1, Math.round((max - min) / (desiredTicks - 1))); + const ticks = []; + for (let i = 0; i < desiredTicks; i++) { + ticks.push(min + i * intervalMs); + } + // Ensure last tick is exactly max + if (ticks[ticks.length - 1] !== max) ticks[ticks.length - 1] = max; + return ticks; +}; + +export const useChartColors = () => { + const theme = useTheme(); + const isDark = theme.palette.mode === ThemeMode.DARK; + + return { + primary: isDark ? "#8884d8" : "#6366f1", + secondary: isDark ? "#82ca9d" : "#10b981", + tertiary: isDark ? "#ff7300" : "#f59e0b", + error: isDark ? "#ff5252" : "#ef4444", + success: isDark ? "#4caf50" : "#22c55e", + grid: isDark ? "#333" : "#ddd", + text: isDark ? "#ccc" : "#333", + isDark, + }; +}; diff --git a/ui-next/src/components/charts/index.ts b/ui-next/src/components/charts/index.ts new file mode 100644 index 0000000000..9f4a96622b --- /dev/null +++ b/ui-next/src/components/charts/index.ts @@ -0,0 +1,9 @@ +export { CacheChart } from "./CacheChart"; +export { + ChartType, + type BaseChartProps, + type LatencyChartProps, +} from "./chartUtils"; +export { ErrorsChart } from "./ErrorsChart"; +export { LatencyChart } from "./LatencyChart"; +export { RequestsChart } from "./RequestsChart"; diff --git a/ui-next/src/components/coPilot/CoPilot.tsx b/ui-next/src/components/coPilot/CoPilot.tsx new file mode 100644 index 0000000000..7f4c927c80 --- /dev/null +++ b/ui-next/src/components/coPilot/CoPilot.tsx @@ -0,0 +1,209 @@ +import ModelTrainingOutlined from "@mui/icons-material/ModelTrainingOutlined"; +import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline"; +import MinimizeOutlinedIcon from "@mui/icons-material/MinimizeOutlined"; +import InsertEmoticonOutlinedIcon from "@mui/icons-material/InsertEmoticonOutlined"; +import AndroidOutlinedIcon from "@mui/icons-material/AndroidOutlined"; +import { Box } from "@mui/material"; +import { useRef, useState, useEffect } from "react"; +import { + greyBorder, + greyText, + greyText2, + purple, + white, +} from "theme/tokens/colors"; +import ArrowBox from "components/v1/ArrowBox"; +import { RoundedInput } from "components/v1/RoundedInput"; +import ChatOutlinedIcon from "@mui/icons-material/ChatOutlined"; + +const boxStyle = (toggle: boolean) => { + return { + position: "fixed", + left: 15, + bottom: -4, + borderRadius: "6px", + width: "100%", + maxWidth: "284px", + maxHeight: "474px", + height: toggle ? "100%" : "auto", + boxShadow: "4px 4px 10px 0px rgba(89, 89, 89, 0.41)", + padding: "4px", + background: white, + }; +}; + +const headerStyle = { + padding: "10px", + display: "flex", + alignItems: "center", + fontSize: "14px", + fontWeight: 600, +}; + +const controlStyle = { + fontSize: "8px", + display: "flex", + alignItems: "center", + position: "absolute", + right: 10, + cursor: "pointer", +}; +const contentStyle = { + marginTop: "20px", + paddingBottom: "6px", + height: "360px", + overflow: "auto", +}; +const footerStyle = { + padding: "10px 7px", + backgroundImage: "linear-gradient(180deg, white, white)", +}; +const userStyle = { + display: "flex", + alignItems: "center", +}; + +const arrowBox1Args = { + children: + "Create a workflow to send flowers to my mother. Don't spend over $100.", + position: "right", + backgroundColor: "#F4EEFF", + borderColor: purple, +}; +const arrowBox2Args = { + children: "Here is a workflow template for sending flowers.", + position: "left", +}; + +const UserChat = () => { + return ( + + + + + + + + + + Me + + 1 min ago + + + + + ); +}; + +const CoPilotChat = () => { + return ( + + + + + + + CoPilot + + Just now + + + + + + + + ); +}; + +function CoPilot() { + const [toggle, setToggle] = useState(false); + const contentRef = useRef(null); + const handleToggle = () => { + setToggle(!toggle); + }; + + useEffect(() => { + if (contentRef.current) { + contentRef.current.scroll({ + top: contentRef.current?.scrollHeight, + behavior: "smooth", + }); + } + }, [toggle]); + + return ( + + + + + + CoPilot + {/* maximize */} + + + {toggle ? ( + + ) : ( + + )} + + {toggle ? "Minimize" : "Open"} + + + {toggle && ( + <> + + + + + + } + /> + + + )} + + ); +} + +export default CoPilot; diff --git a/ui-next/src/components/conductorTooltip/ConductorTooltip.tsx b/ui-next/src/components/conductorTooltip/ConductorTooltip.tsx new file mode 100644 index 0000000000..bad0512d95 --- /dev/null +++ b/ui-next/src/components/conductorTooltip/ConductorTooltip.tsx @@ -0,0 +1,59 @@ +import TooltipStateless from "components/v1/TooltipStateless"; +import { TooltipProps } from "@mui/material"; +import { ReactNode, useEffect, useState } from "react"; + +interface ConductorTooltipProps extends Omit { + title: string; + content: ReactNode; + showInitial?: boolean; + initialTimeout?: number; + onClose?: () => void; +} + +function ConductorTooltip({ + title, + content, + children, + placement, + showInitial, + initialTimeout = 1000, + onClose, +}: ConductorTooltipProps) { + const [open, setOpen] = useState(false); + const handleClose = () => { + setOpen(false); + if (onClose) { + onClose(); + } + }; + const handleOpen = (value: boolean) => { + setOpen(value); + }; + + useEffect(() => { + let timeoutId: any; + if (showInitial) { + setOpen(true); + timeoutId = setTimeout(() => { + handleClose(); + }, initialTimeout); + } + return () => clearTimeout(timeoutId); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [initialTimeout]); + + return ( + + ); +} + +export default ConductorTooltip; +export type { ConductorTooltipProps }; diff --git a/ui-next/src/components/definitionList/DefinitionList.jsx b/ui-next/src/components/definitionList/DefinitionList.jsx new file mode 100644 index 0000000000..e9c091c017 --- /dev/null +++ b/ui-next/src/components/definitionList/DefinitionList.jsx @@ -0,0 +1,19 @@ +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import { styled } from "@mui/material/styles"; + +const StyledTableBody = styled(TableBody)(({ theme }) => ({ + "& tr:first-child": { + borderTopColor: theme.palette.divider, + borderTopStyle: "solid", + borderTopWidth: "1px", + }, +})); + +const DefinitionList = ({ children }) => ( + + {children} +
    +); + +export default DefinitionList; diff --git a/ui-next/src/components/diagram/diagram.scss b/ui-next/src/components/diagram/diagram.scss new file mode 100644 index 0000000000..32cd64c511 --- /dev/null +++ b/ui-next/src/components/diagram/diagram.scss @@ -0,0 +1,198 @@ +$dark-color: #333; +$light-color: #c8c8c8; /* gray11*/ +$white: #fff; +$edge-label-color: blue; +$outline-width: 0.6px; +$node-text-size: 12px; + +.graphContainer { + padding: 10px; +} + +.graphSvg { + width: 100%; + min-height: 600px; +} + +@mixin nodeColor($colorfg, $colorbg: #fff) { + &.bar { + rect { + stroke: $colorfg !important; + fill: $colorfg; + } + } + text { + fill: $colorfg; + } + rect, + polygon, + circle { + fill: $colorbg; + stroke: $colorfg; + } +} + +.node { + &:hover { + rect, + polygon { + filter: url("#brightness"); + } + } + + text { + fill: $dark-color; + font-size: 13px; + pointer-events: none; + } + + rect, + circle, + polygon { + stroke: $dark-color; + fill: $white; + stroke-width: $outline-width; + } + + rect { + rx: 5px; + ry: 5px; + } + + &.type-DO_WHILE { + ellipse { + fill: $light-color; + } + } + + &.type-SUB_WORKFLOW { + rect { + stroke-width: 5px; + } + } + &.type-TERMINAL { + circle { + stroke: $dark-color; + fill: #eee; + stroke-width: 0.6px; + } + text { + color: $dark-color; + font-weight: bold; + } + &.dimmed circle { + stroke: $light-color; + } + } + + &.dimmed { + @include nodeColor($light-color); + } + &.status_COMPLETED { + @include nodeColor(#163e1d, #aee1b8); + } + &.status_COMPLETED_WITH_ERRORS { + @include nodeColor(#8b5b02, #feeac5); + } + &.status_IN_PROGRESS, + &.status_SCHEDULED { + @include nodeColor(#11497a, #cbe2f7); + } + //&.status_CANCELED { @include nodeColor(#26194b, #ded5f8); } + &.status_FAILED, + &.status_FAILED_WITH_TERMINAL_ERROR, + &.status_TIMED_OUT, + &.status_DF_PARTIAL, + &.status_CANCELED { + @include nodeColor(#7f050b, #f9c6c9); + } + &.status_SKIPPED { + @include nodeColor(gray); + } + &.selected { + filter: url("#dropShadow"); + } +} + +.node.bar { + &.type-FORK_JOIN_DYNAMIC { + rect { + stroke: $dark-color; + stroke-width: 5; + stroke-dasharray: 10; + } + &.dimmed { + rect { + stroke: $light-color; + } + } + } + /* + &.type-EXCLUSIVE_JOIN { + rect { + stroke: $dark-color; + fill: #fff; + stroke-width: $outline-width; + } + rect.underline { + stroke-width: 0; + fill: $dark-color; + } + text { + fill: $dark-color; + } + &.dimmed { + rect { + stroke: $light-color; + fill: #fff; + } + text { + fill: $light-color; + } + } + } +*/ + rect { + rx: 0px; + ry: 0px; + stroke-width: 0; + fill: $dark-color; + } + text { + fill: $white; + } + + &.dimmed { + rect { + fill: $light-color; + } + } +} + +.edgePath { + path { + stroke: $dark-color; + stroke-width: 1px; + } + &.dimmed { + path { + stroke: $light-color; + stroke-dasharray: 5; + } + marker { + fill: $light-color; + } + } + &.executed { + path { + stroke-width: 2px; + } + } +} +.edgeLabel { + fill: $edge-label-color; + font-size: 12px; + &.dimmed text { + fill: $light-color; + } +} diff --git a/ui-next/src/components/flow/Flow.tsx b/ui-next/src/components/flow/Flow.tsx new file mode 100644 index 0000000000..1a74b4390e --- /dev/null +++ b/ui-next/src/components/flow/Flow.tsx @@ -0,0 +1,340 @@ +import { DndContext, MouseSensor, useSensor, useSensors } from "@dnd-kit/core"; +import ConfirmChoiceDialog from "components/ConfirmChoiceDialog"; +import { isForkJoinPathEmpty } from "components/flow/nodes/mapper/forkJoin"; +import { isSwitchPathEmpty } from "components/flow/nodes/mapper/switch"; +import { getFlowTheme } from "components/flow/theme"; +import { WorkflowEditContext } from "pages/definition/state"; +import { buildDataForRemoveBranchOperation } from "pages/definition/state/taskModifier/taskModifier"; +import { usePerformOperationOnDefinition } from "pages/definition/state/usePerformOperationOnDefintion"; +import { ExecutionActionTypes } from "pages/execution/state"; +import { + FunctionComponent, + MouseEvent, + useCallback, + useContext, + useRef, + useState, +} from "react"; +import { Canvas, Edge, EdgeData, NodeData, PortData } from "reaflow"; +import { ColorModeContext } from "theme/material/ColorModeContext"; +import { TaskStatus, TaskType } from "types"; +import { ActorRef } from "xstate"; +import { CustomLabel, CustomNode } from "./components/graphs"; +import PanAndZoomWrapper, { + usePanAndZoomActor, +} from "./components/graphs/PanAndZoomWrapper"; +import { EDGE_SPACING } from "./components/graphs/PanAndZoomWrapper/constants"; +import QuickAddMenu from "./components/RichAddTaskMenu/QuickAddMenu"; +import { DraggableOverlay, useNodeCollisionDetection } from "./dragDrop"; +import { + DraggedNodeData, + FlowEvents, + FlowMachineContextProvider, + useFlowMachine, +} from "./state"; +import { useSelector } from "@xstate/react"; + +import "./ReaflowOverrides.scss"; + +interface FlowProps { + flowActor: ActorRef; + readOnly?: boolean; + leftPanelExpanded: boolean; + isExecutionView?: boolean; +} + +const dashedEdgeStyles = { + stroke: "#b1b1b7", + strokeDasharray: "5", + strokeDashoffset: 10, + strokeWidth: 2, + markerEnd: "none", +}; + +export const Flow: FunctionComponent = ({ + flowActor, + readOnly = false, + leftPanelExpanded, + isExecutionView = false, +}) => { + const mouseSensor = useSensor(MouseSensor, {}); + const sensors = useSensors(mouseSensor); + + const { mode } = useContext(ColorModeContext); + const theme = getFlowTheme(mode); + + const [ + { + selectNode, + toggleEdgeMenu, + toggleNodeMenu, + handleSetLayout, + selectEdge, + draggingStarts, + draggingNodeEnds, + }, + { + nodes, + edges, + openedEdge, + selectedNode, + isInconsistent, + panAndZoomActor, + isShowDescription, + }, + ] = useFlowMachine(flowActor); + + const { workflowDefinitionActor } = useContext(WorkflowEditContext); + const { handleRemoveTask: onRemoveTask, handleRemoveBranch: onRemoveBranch } = + usePerformOperationOnDefinition(workflowDefinitionActor!); + + const [showConfirmDialog, setShowConfirmDialog] = useState(false); + const [showConfirmDeletePathDialog, setShowConfirmDeletePathDialog] = + useState(false); + const edgeAnchorEl = useRef(null); + const nodeAnchorEl = useRef(null); + const canvasRef = useRef(null); + + const [selectedOperationContext, setSelectedOperationContext] = + useState(null); + + const onNodeClick = useCallback( + (event: MouseEvent, node: NodeData) => { + const { target } = event; + const targetElement = target as HTMLElement; + + if (!isInconsistent) { + const className = targetElement.className; + if (className.includes("DeleteButton")) { + event.preventDefault(); + nodeAnchorEl.current = targetElement; + setShowConfirmDialog(true); + } else { + event.stopPropagation(); + } + selectNode(node); + } + }, + [selectNode, isInconsistent], + ); + + const onToggleMenuClick = useCallback( + (event: MouseEvent, edge: EdgeData) => { + edgeAnchorEl.current = event.target as HTMLElement; + toggleEdgeMenu(edge); + event.stopPropagation(); + }, + [toggleEdgeMenu], + ); + const collisionDetection = useNodeCollisionDetection(panAndZoomActor!); + + const [ + { notifiedEventType, viewportSize }, + { handleSetEventType, handleCenterOnSelectedTask }, + ] = usePanAndZoomActor(panAndZoomActor); + + const richAddTaskMenuActor = (flowActor as any)?.children?.get( + "richAddTaskMenuMachine", + ); + + const operationContext = useSelector( + richAddTaskMenuActor || flowActor, + (state: { context: { operationContext?: any } }) => + richAddTaskMenuActor ? state.context.operationContext : undefined, + ); + + return ( + + {!readOnly && ( + <> + {showConfirmDialog && ( + { + if ( + confirmed && + selectedNode?.data?.task != null && + selectedNode?.data?.crumbs != null + ) { + onRemoveTask(selectedNode.data); + } + setShowConfirmDialog(false); + }} + message={"Are you sure you want to delete this task?"} + /> + )} + {showConfirmDeletePathDialog && ( + { + if (confirmed && selectedOperationContext) { + onRemoveBranch(selectedOperationContext); + } + + setShowConfirmDeletePathDialog(false); + setSelectedOperationContext(null); + }} + message={ + <> + Are you sure you want to delete the path  + {selectedOperationContext?.branchName} ? + + } + /> + )} + {openedEdge ? ( + + ) : null} + + )} + {panAndZoomActor && ( + { + draggingNodeEnds( + event?.active?.data?.current as DraggedNodeData, + event?.over?.data?.current as DraggedNodeData, + ); + }} + onDragStart={(event) => { + if (event?.active?.data?.current) { + draggingStarts(event?.active?.data?.current as DraggedNodeData); + } + }} + sensors={sensors} + collisionDetection={collisionDetection} + > + } + flowActor={flowActor} + isExecutionView={isExecutionView} + > + { + const edgeStylesForTaskStatus = [ + TaskStatus.COMPLETED, + TaskStatus.COMPLETED_WITH_ERRORS, + ].includes(edge?.data?.status) + ? { + stroke: theme.edges.completed.stroke, + strokeWidth: theme.edges.completed.strokeWidth, + } + : { + stroke: theme.edges.default.stroke, + strokeWidth: theme.edges.default.strokeWidth, + }; + const edgeStyles = + edge?.data?.unreachableEdge === true || + edge?.data?.delayedEdge === true + ? dashedEdgeStyles + : edgeStylesForTaskStatus; + return ( + + } + style={edgeStyles} + /> + ); + }} + node={ + , + port: PortData, + ) => { + onToggleMenuClick(event, port); + }} + onDeleteBranch={( + __event: any, + { + port, + node, + }: { + port: PortData; + node: NodeData; + } /* The type is better defined in core modules*/, + ) => { + const operationContext = buildDataForRemoveBranchOperation({ + port, + node, + }); + + if ( + operationContext?.task?.type === TaskType.SWITCH && + !isSwitchPathEmpty( + operationContext?.branchName, + operationContext?.task, + ) + ) { + setSelectedOperationContext(operationContext); + setShowConfirmDeletePathDialog(true); + } else if ( + operationContext?.task?.type === TaskType.FORK_JOIN && + !isForkJoinPathEmpty( + operationContext?.branchName, + operationContext?.task, + ) + ) { + setSelectedOperationContext(operationContext); + setShowConfirmDeletePathDialog(true); + } else { + onRemoveBranch(operationContext); + } + }} + isInconsistent={isInconsistent} + /> + } + onLayoutChange={(layout) => { + if (layout != null && layout.width != null) { + handleSetLayout(layout); + + if ( + notifiedEventType === + ExecutionActionTypes.COLLAPSE_DYNAMIC_TASK + ) { + // Reset notified event type + handleSetEventType(""); + + // Center selected task + handleCenterOnSelectedTask( + viewportSize.width, + viewportSize.height, + ); + } + } + }} + /> + + + )} + + ); +}; diff --git a/ui-next/src/components/flow/FlowFullscreen.scss b/ui-next/src/components/flow/FlowFullscreen.scss new file mode 100644 index 0000000000..dd6d2f1f84 --- /dev/null +++ b/ui-next/src/components/flow/FlowFullscreen.scss @@ -0,0 +1,5 @@ +.FlowFullscreen { + flex-grow: 2; + display: flex; + overflow: hidden; +} diff --git a/ui-next/src/components/flow/ReaflowOverrides.scss b/ui-next/src/components/flow/ReaflowOverrides.scss new file mode 100644 index 0000000000..2c87f11265 --- /dev/null +++ b/ui-next/src/components/flow/ReaflowOverrides.scss @@ -0,0 +1,5 @@ +/* Disable pointer events for all edges */ +g[class^="Edge-module_edge"], +g[class*=" Edge-module_edge"] { + pointer-events: none; +} diff --git a/ui-next/src/components/flow/components/RichAddTaskMenu/AddTaskSidebar.tsx b/ui-next/src/components/flow/components/RichAddTaskMenu/AddTaskSidebar.tsx new file mode 100644 index 0000000000..1efc60ce3a --- /dev/null +++ b/ui-next/src/components/flow/components/RichAddTaskMenu/AddTaskSidebar.tsx @@ -0,0 +1,867 @@ +import { + alpha, + Box, + Button, + CircularProgress, + Grid, + IconButton, + InputBase, + Typography, +} from "@mui/material"; +import { + ArrowRight, + Cpu, + Gear, + GridFour as GridLines, + MagnifyingGlass, + Plus, + Robot, + Users, + X, +} from "@phosphor-icons/react"; +import { IntegrationIcon } from "components/IntegrationIcon"; +import { MessageContext } from "components/v1/layout/MessageContext"; +import { WorkflowEditContext } from "pages/definition/state"; +import { buildDataForOperation } from "pages/definition/state/taskModifier/taskModifier"; +import { DefinitionMachineEventTypes } from "pages/definition/state/types"; +import { usePerformOperationOnDefinition } from "pages/definition/state/usePerformOperationOnDefintion"; +import { pluginRegistry } from "plugins/registry"; +import React, { + cloneElement, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { + BaseIntegration, + CommonTaskDef, + IntegrationDef, + SubWorkflowTaskDef, +} from "types"; +import { getSequentiallySuffix } from "utils/strings"; +import { getInitials } from "utils/utils"; +import { ActorRef } from "xstate"; +import { itemFilterMatcher } from "./helpers"; +import { iconForTaskTypeMap } from "./iconsForTaskTypes"; +import { IntegrationDrillDownContent } from "./IntegrationDrillDownContent"; +import { useRichAddTaskMenu } from "./state/hook"; +import { + BaseTaskMenuItem, + IntegrationMenuItem, + TaskMenuItem as OriginalTaskMenuItem, + RichAddMenuTabs, + RichAddTaskMenuEvents, + RichAddTaskMenuEventTypes, +} from "./state/types"; +import { getALL_TASKS } from "./supportedTasks"; +import { + generateMCPTask, + generateSimpleTask, + generateSubWorkflowTask, + NameGeneratorFn, + taskGeneratorMap, +} from "./taskGenerator"; + +// Extend the TaskMenuItem type to include status and onClick +type TaskMenuItem = Omit & { + status?: string; + onClick?: () => void; + icon?: React.ReactElement; +}; +type AddTaskSidebarProps = { + open: boolean; + setOpen?: (val: boolean) => void; + richAddTaskMenuActor: ActorRef; +}; + +const noRandomSuffix: NameGeneratorFn = (aPram: string) => ({ + name: `${aPram}`, + taskReferenceName: `${aPram}_ref`, +}); + +const SIDEBAR_ITEMS = [ + { + label: "All", + tab: RichAddMenuTabs.ALL_TAB, + icon: GridLines, + }, + { + label: "System", + tab: RichAddMenuTabs.SYSTEMS_TAB, + icon: Cpu, + }, + { + label: "AI", + tab: RichAddMenuTabs.AI_AGENTS_TAB, + icon: Robot, + }, + { + label: "Worker Tasks", + tab: RichAddMenuTabs.WORKERS_TAB, + icon: Users, + }, + { + label: "Integrations", + tab: RichAddMenuTabs.INTEGRATIONS_TAB, + icon: Gear, + }, +]; + +const AddTaskSidebar = ({ + open, + setOpen, + richAddTaskMenuActor, +}: AddTaskSidebarProps) => { + const { workflowDefinitionActor } = useContext(WorkflowEditContext); + const { setMessage } = useContext(MessageContext); + const listRef = useRef(null); + const { handlePerformOperation: onPerformOperation } = + usePerformOperationOnDefinition(workflowDefinitionActor!); + + const [ + { + supportedIntegrations, + integrationDefs, + integrationDrillDownMenu, + scrollPosition, + operationContext, + nodes, + workerMenuItems, + subWorkflowMenuItems, + selectedTab, + isFetching, + searchQuery, + }, + { refetchIntegrations, handleUpdateIntegrationDrillDown, handleTyping }, + ] = useRichAddTaskMenu(richAddTaskMenuActor); + + const send = richAddTaskMenuActor?.send; + + const [basicIntegrationTemplate, _setBasicIntegrationTemplate] = useState< + BaseIntegration | undefined + >(undefined); + + const setBasicIntegrationTemplate = useCallback( + (template?: BaseIntegration) => { + if (!template) { + _setBasicIntegrationTemplate(undefined); + return; + } + const templateNameWithoutSpaces = { + ...template, + name: template.name.replace(/\s+/g, ""), + }; + _setBasicIntegrationTemplate(templateNameWithoutSpaces); + }, + [_setBasicIntegrationTemplate], + ); + + const taskRefNames: string[] = useMemo( + () => nodes.map((node) => node?.data?.task?.taskReferenceName), + [nodes], + ); + + const handleEditModelClose = () => { + setBasicIntegrationTemplate(undefined); + }; + + const handleIntegrationSave = () => { + setMessage({ + text: "Integration created successfully", + severity: "success", + }); + setBasicIntegrationTemplate(undefined); + refetchIntegrations(); + }; + + // Add scroll handler + const handleScroll = (event: any) => { + const x = event.currentTarget.scrollTop + event.currentTarget.clientHeight; + if ( + event.currentTarget.scrollHeight - x <= 1 && + selectedTab === RichAddMenuTabs.ALL_TAB + ) { + send({ + type: RichAddTaskMenuEventTypes.GOT_TO_END, + lastScrollTopPosition: event.currentTarget.scrollTop, + }); + } + }; + + // Add effect to restore scroll position + useEffect(() => { + if (listRef.current) { + listRef.current.scrollTop = scrollPosition ?? 0; + } + }, [scrollPosition]); + + type TaskGeneratorFn = () => CommonTaskDef | CommonTaskDef[]; + + const handleAddTaskBelow = useCallback( + (payloadGenFn: TaskGeneratorFn) => () => { + const dataForOperation = buildDataForOperation( + operationContext?.port, + operationContext?.node, + ); + + onPerformOperation({ + ...dataForOperation, + operation: { + payload: payloadGenFn(), + }, + }); + }, + [onPerformOperation, operationContext], + ); + + const getSequentialTask = useCallback( + ({ + handler, + overrides, + }: { + handler: any; + overrides?: Partial; + }) => { + let newTask = handler({ + overrides, + nameGenerator: noRandomSuffix, + }); + + if (Array.isArray(newTask)) { + newTask = newTask.map((task) => { + const sequentialName = getSequentiallySuffix({ + name: task.taskReferenceName, + refNames: taskRefNames, + }); + + return { + ...task, + name: overrides?.name ? task.name : sequentialName.name, + taskReferenceName: sequentialName.taskReferenceName, + }; + }); + + return newTask; + } + + const sequentialName = getSequentiallySuffix({ + name: newTask.taskReferenceName, + refNames: taskRefNames, + }); + + return { + ...newTask, + name: overrides?.name ? newTask?.name : sequentialName.name, + taskReferenceName: sequentialName.taskReferenceName, + }; + }, + [taskRefNames], + ); + + const taskOptions = getALL_TASKS().map((bt: BaseTaskMenuItem) => { + const IconComponent = iconForTaskTypeMap[bt.type]; + const generatorForType = taskGeneratorMap[bt.type]; + + return { + category: bt.category, + name: bt.name, + description: bt.description, + onClick: handleAddTaskBelow(() => + getSequentialTask({ + handler: generatorForType, + }), + ), + icon: , + }; + }); + + const handleChangeTab = (data: RichAddMenuTabs) => { + handleCloseDrillDown(); + send({ + type: RichAddTaskMenuEventTypes.SET_SELECTED_TAB, + tab: data, + }); + }; + + const workerOptions = useMemo( + () => + workerMenuItems.map((baseItem: BaseTaskMenuItem) => ({ + ...baseItem, + onClick: handleAddTaskBelow(() => + getSequentialTask({ + overrides: { + name: baseItem.name, + taskReferenceName: `${baseItem.name}_ref`, + }, + handler: generateSimpleTask, + }), + ), + })), + [getSequentialTask, handleAddTaskBelow, workerMenuItems], + ); + + const workflowDefinitionsOptions = useMemo( + () => + subWorkflowMenuItems.map((baseItem: BaseTaskMenuItem) => ({ + ...baseItem, + onClick: handleAddTaskBelow(() => + getSequentialTask({ + overrides: { + name: baseItem.name, + taskReferenceName: `${baseItem.name}_ref`, + subWorkflowParam: { + name: baseItem.name, + version: baseItem.version, + }, + }, + handler: generateSubWorkflowTask, + }), + ), + })), + [subWorkflowMenuItems, handleAddTaskBelow, getSequentialTask], + ); + + const options: TaskMenuItem[] = useMemo( + () => + [ + ...taskOptions, + ...workerOptions, + ...workflowDefinitionsOptions, + ...supportedIntegrations, + ] as TaskMenuItem[], + [ + taskOptions, + workerOptions, + workflowDefinitionsOptions, + supportedIntegrations, + ], + ); + + const filteredOptions = useMemo(() => { + if (options) { + const filterer = itemFilterMatcher(searchQuery, selectedTab); + return options.filter(filterer); + } else return []; + }, [selectedTab, searchQuery, options]); + + // Add a function to generate unique task ID + const getTaskUniqueId = (task: TaskMenuItem) => + `${task.name}_${task.category}_${task.description}`; + + const handleTaskClick = (task: TaskMenuItem) => { + if ( + task.category === RichAddMenuTabs.INTEGRATIONS_TAB && + task.status === "active" + ) { + handleTyping(""); + handleUpdateIntegrationDrillDown({ + isOpen: true, + selectedRootIntegration: task as IntegrationMenuItem, + level: "integrations", + selectedIntegration: null, + }); + // handleFetchIntegrationTools(task as IntegrationMenuItem); + } else if ( + task.category === RichAddMenuTabs.INTEGRATIONS_TAB && + task.status !== "active" + ) { + const template = integrationDefs.find( + (integration: IntegrationDef) => integration.name === task?.name, + ); + setBasicIntegrationTemplate({ + name: template?.name, + description: "", + type: template?.type, + category: template?.category, + enabled: template?.enabled, + }); + } else if (task.onClick) { + task.onClick(); + } + }; + + const handleCloseMenu = useCallback(() => { + send({ type: RichAddTaskMenuEventTypes.CLOSE_MENU }); + }, [send]); + + const handleClose = useCallback(() => { + if (setOpen) { + setOpen(false); + } + if (workflowDefinitionActor) { + workflowDefinitionActor.send({ + type: DefinitionMachineEventTypes.HANDLE_LEFT_PANEL_EXPANDED, + onSelectNode: false, + }); + } + handleCloseMenu(); + }, [handleCloseMenu, setOpen, workflowDefinitionActor]); + + const handleAddToolTask = (tool: any) => { + return handleAddTaskBelow(() => + getSequentialTask({ + overrides: { + name: tool?.api, + taskReferenceName: `${tool?.api}_ref`, + description: tool?.description, + inputParameters: { + integrationName: + integrationDrillDownMenu?.selectedIntegration?.name, + method: tool?.api, + integrationType: + integrationDrillDownMenu?.selectedIntegration?.integrationType, + // ...generateObjectFromSchema(tool?.inputSchema?.data), + }, + }, + handler: generateMCPTask, + }), + )(); + }; + + const handleCloseDrillDown = () => { + handleUpdateIntegrationDrillDown({ + isOpen: false, + selectedIntegration: null, + selectedRootIntegration: null, + level: "integrations", + }); + }; + + return open ? ( + + + {/* Header */} + + + Add Task + + + + + {/* Search */} + + + + 🔍 + + handleTyping(e.target.value)} + sx={{ + flex: 1, + fontSize: "0.875rem", + "& input": { + padding: 0, + }, + }} + endAdornment={ + searchQuery ? ( + handleTyping("")} + sx={{ + color: "#6B7280", + p: 0.5, + "&:hover": { + color: "#4B5563", + backgroundColor: "transparent", + }, + }} + > + + + ) : null + } + /> + {!integrationDrillDownMenu.isOpen && ( + + {filteredOptions.length} results + + )} + + + + {/* Categories */} + + + {SIDEBAR_ITEMS.map((item) => ( + + handleChangeTab(item.tab)} + sx={{ + display: "flex", + flexDirection: "column", + alignItems: "center", + gap: 0.75, + py: 1, + px: 0.5, + cursor: "pointer", + borderRadius: "6px", + backgroundColor: + selectedTab === item.tab ? "#F3F4F6" : "transparent", + transition: "all 0.2s ease", + border: "1px solid", + borderColor: + selectedTab === item.tab ? "#E5E7EB" : "transparent", + "&:hover": { + backgroundColor: + selectedTab === item.tab ? "#F3F4F6" : "#F9FAFB", + borderColor: "#E5E7EB", + }, + }} + > + + + {item.label} + + + + ))} + + + + {/* Task List */} + + {isFetching ? ( + + + + ) : !integrationDrillDownMenu.isOpen && + filteredOptions.length === 0 ? ( + + + + No tasks found + + + ) : ( + <> + + {!integrationDrillDownMenu.isOpen && + filteredOptions.map( + (task: TaskMenuItem | IntegrationMenuItem, idx: number) => ( + + handleTaskClick(task)} + sx={{ + background: "#FFFFFF", + border: "1px solid #F0F0F0", + borderRadius: 2, + p: 1.5, + cursor: "pointer", + transition: "all 0.2s ease", + boxShadow: "0 1px 2px rgba(0, 0, 0, 0.05)", + "&:hover": { + backgroundColor: "#F9FAFB", + borderColor: "#E5E7EB", + transform: "translateY(-1px)", + boxShadow: + "0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03)", + "& .task-icon-box": { + backgroundColor: alpha("#3B82F6", 0.08), + color: "#3B82F6", + "& .task-icon-typography": { + color: "#3B82F6", + }, + }, + "& .task-description": { + color: "#1E293B", + }, + }, + }} + > + + + {cloneElement( + "iconName" in task && + (task.iconName || task?.integrationType) ? ( +
    + +
    + ) : "icon" in task && task.icon ? ( + task.icon + ) : ( + + {getInitials(task.name)} + + ), + { + size: 20, + }, + )} +
    + + + {task.name} + + {task.category !== + RichAddMenuTabs.INTEGRATIONS_TAB && ( + + {task.description} + + )} + + {task.category === + RichAddMenuTabs.INTEGRATIONS_TAB && + ((task as any)?.status === "active" ? ( + + ) : ( + + ))} +
    +
    +
    + ), + )} +
    + {integrationDrillDownMenu.isOpen && + integrationDrillDownMenu.selectedRootIntegration && ( + { + setBasicIntegrationTemplate({ + name: template?.name, + description: "", + type: template?.type, + category: template?.category, + enabled: template?.enabled, + }); + }} + /> + )} + + )} +
    +
    + {basicIntegrationTemplate && + (() => { + const IntegrationEditModal = pluginRegistry.getNewIntegrationModal(); + return IntegrationEditModal ? ( + + ) : null; + })()} +
    + ) : null; +}; + +export default AddTaskSidebar; diff --git a/ui-next/src/components/flow/components/RichAddTaskMenu/IntegrationDrillDownContent.tsx b/ui-next/src/components/flow/components/RichAddTaskMenu/IntegrationDrillDownContent.tsx new file mode 100644 index 0000000000..104ae9b7ad --- /dev/null +++ b/ui-next/src/components/flow/components/RichAddTaskMenu/IntegrationDrillDownContent.tsx @@ -0,0 +1,429 @@ +import React from "react"; +import { + Box, + Typography, + CircularProgress, + IconButton, + Button, +} from "@mui/material"; +import { ArrowRight, Plus, MagnifyingGlass } from "@phosphor-icons/react"; +import { IntegrationMenuItem } from "./state/types"; +import { IntegrationIcon } from "components/IntegrationIcon"; +import { getInitials } from "utils/utils"; +import { IntegrationDef } from "types"; +import { useRichAddTaskMenu } from "./state/hook"; +import { ActorRef } from "xstate"; +import { RichAddTaskMenuEvents } from "./state/types"; + +interface IntegrationDrillDownContentProps { + richAddTaskMenuActor: ActorRef; + onAddToolTask: (tool: any) => void; + onAddNewIntegration: (integration: IntegrationDef) => void; +} + +export const IntegrationDrillDownContent: React.FC< + IntegrationDrillDownContentProps +> = ({ richAddTaskMenuActor, onAddToolTask, onAddNewIntegration }) => { + const [ + { + availableIntegrations, + integrationDefs, + integrationDrillDownMenu, + isFetchingIntegrationTools, + searchQuery, + }, + { + handleFetchIntegrationTools, + handleUpdateIntegrationDrillDown, + handleTyping, + }, + ] = useRichAddTaskMenu(richAddTaskMenuActor); + + if ( + !integrationDrillDownMenu?.isOpen || + !integrationDrillDownMenu?.selectedRootIntegration + ) { + return null; + } + + const { + level, + selectedIntegration, + selectedRootIntegration, + selectedIntegrationTools, + } = integrationDrillDownMenu; + + const handleNavigateToTools = (integration: IntegrationMenuItem) => { + handleTyping(""); + handleFetchIntegrationTools(integration); + }; + + const handleNavigateBack = () => { + handleTyping(""); + handleUpdateIntegrationDrillDown({ + ...integrationDrillDownMenu, + selectedIntegration: null, + selectedRootIntegration: + integrationDrillDownMenu?.level === "tools" + ? integrationDrillDownMenu?.selectedRootIntegration + : null, + selectedIntegrationTools: null, + level: "integrations", + }); + }; + + const handleCloseDrillDown = () => { + handleUpdateIntegrationDrillDown({ + isOpen: false, + selectedIntegration: null, + selectedRootIntegration: null, + level: "integrations", + }); + }; + + const filterBySearchQuery = (items: any[], fields: string[]) => { + if (!searchQuery) return items; + const query = searchQuery?.toLowerCase(); + return items?.filter((item) => + fields?.some((field) => item[field]?.toLowerCase()?.includes(query)), + ); + }; + const filteredIntegrations = filterBySearchQuery( + (availableIntegrations || []).filter( + (integration: IntegrationMenuItem) => + integration?.integrationType === + selectedRootIntegration?.integrationType, + ), + ["name"], + ); + const filteredTools = filterBySearchQuery(selectedIntegrationTools || [], [ + "api", + ]); + + if (level === "integrations") { + return ( + + + + + + + {selectedRootIntegration?.name} + + + + + + + + Integrations ({filteredIntegrations?.length}) + + + + + {filteredIntegrations?.length === 0 ? ( + + + + No integrations found + + + ) : ( + filteredIntegrations?.map( + (integration: IntegrationMenuItem, idx: number) => ( + handleNavigateToTools(integration)} + > + + + + + + + + + {integration?.name} + + + {integration?.description} + + + + + + ), + ) + )} + + + ); + } + + if (level === "tools") { + return ( + + + + + + + {selectedRootIntegration?.name} - {selectedIntegration?.name} + + + + + Tools ({filteredTools?.length}) + + + + + {isFetchingIntegrationTools ? ( + + + + ) : filteredTools?.length === 0 ? ( + + + + No tools found + + + ) : ( + filteredTools?.map((tool: any, idx: number) => ( + onAddToolTask(tool)} + > + + {tool?.integrationType ? ( +
    + +
    + ) : ( + + {getInitials(tool?.api)} + + )} +
    + + + {tool?.api} + + + {tool?.description} + + +
    + )) + )} +
    +
    + ); + } + + return null; +}; diff --git a/ui-next/src/components/flow/components/RichAddTaskMenu/QuickAddMenu.tsx b/ui-next/src/components/flow/components/RichAddTaskMenu/QuickAddMenu.tsx new file mode 100644 index 0000000000..70799dba4a --- /dev/null +++ b/ui-next/src/components/flow/components/RichAddTaskMenu/QuickAddMenu.tsx @@ -0,0 +1,917 @@ +import { + alpha, + Box, + Button, + CircularProgress, + ClickAwayListener, + Grid, + IconButton, + InputBase, + Popper, + Tooltip, + Typography, +} from "@mui/material"; +import { + Check, + DotsThree, + GearIcon, + MagnifyingGlass, + X, +} from "@phosphor-icons/react"; +import { useSelector } from "@xstate/react"; +import { WorkflowEditContext } from "pages/definition/state"; +import { buildDataForOperation } from "pages/definition/state/taskModifier/taskModifier"; +import { usePerformOperationOnDefinition } from "pages/definition/state/usePerformOperationOnDefintion"; +import { pluginRegistry } from "plugins/registry"; +import React, { + cloneElement, + ReactElement, + useCallback, + useContext, + useMemo, +} from "react"; +import { NodeData } from "reaflow"; +import { CommonTaskDef, SubWorkflowTaskDef, TaskType } from "types"; +import useArrowNavigation from "useArrowNavigation"; +import { getSequentiallySuffix } from "utils/strings"; +import { getInitials } from "utils/utils"; +import { ActorRef } from "xstate"; +import { itemFilterMatcher } from "./helpers"; +import { iconForTaskTypeMap } from "./iconsForTaskTypes"; +import { useRichAddTaskMenu } from "./state/hook"; +import { + BaseTaskMenuItem, + MainStates, + RichAddMenuTabs, + RichAddTaskMenuEventTypes, +} from "./state/types"; +import { getALL_TASKS } from "./supportedTasks"; +import { + generateSimpleTask, + generateSubWorkflowTask, + taskGeneratorMap, + uniqueTaskIdGenerator, +} from "./taskGenerator"; + +// Core OSS task types that always appear in the quick-add grid (in order) +const OSS_QUICK_ADD_TYPES: TaskType[] = [ + // row 1 + TaskType.SIMPLE, + TaskType.HTTP, + TaskType.HTTP_POLL, + TaskType.GRPC, + TaskType.EVENT, + // row 2 + TaskType.SWITCH, + TaskType.FORK_JOIN, + TaskType.DO_WHILE, + TaskType.SET_VARIABLE, + TaskType.WAIT, + // row 3 + TaskType.SUB_WORKFLOW, + TaskType.START_WORKFLOW, + TaskType.TERMINATE, + TaskType.INLINE, +]; + +// AI/LLM task types for the Agentic Orchestration section +const AI_QUICK_ADD_TYPES: TaskType[] = [ + TaskType.LLM_CHAT_COMPLETE, + TaskType.LLM_TEXT_COMPLETE, + TaskType.LLM_GENERATE_EMBEDDINGS, + TaskType.LLM_GET_EMBEDDINGS, + TaskType.LLM_INDEX_DOCUMENT, + TaskType.LLM_SEARCH_INDEX, +]; + +const noRandomSuffix = (aPram: string) => ({ + name: `${aPram}`, + taskReferenceName: `${aPram}_ref`, +}); + +interface QuickAddMenuProps { + anchorEl: HTMLElement | null; + richAddTaskMenuActor: ActorRef; +} + +type TaskMenuItem = BaseTaskMenuItem & { + status?: string; + onClick?: () => void; + icon?: ReactElement; +}; + +const popperStyle = { + width: "360px", + boxShadow: "0px 8px 24px rgba(0, 0, 0, 0.12)", + borderRadius: "20px", + backgroundColor: "#FFFFFF", + overflow: "hidden", +}; + +const QuickAddMenu = ({ + anchorEl, + richAddTaskMenuActor, +}: QuickAddMenuProps) => { + const { workflowDefinitionActor } = useContext(WorkflowEditContext); + const { handlePerformOperation: onPerformOperation } = + usePerformOperationOnDefinition(workflowDefinitionActor!); + + const searchQuery = useSelector( + richAddTaskMenuActor, + (state) => state.context.searchQuery, + ); + + const handleSearchChange = useCallback( + (value: string) => { + richAddTaskMenuActor.send({ + type: RichAddTaskMenuEventTypes.TYPING, + text: value, + }); + }, + [richAddTaskMenuActor], + ); + + const handleClose = useCallback(() => { + richAddTaskMenuActor.send({ type: RichAddTaskMenuEventTypes.CLOSE_MENU }); + }, [richAddTaskMenuActor]); + + const operationContext = useSelector( + richAddTaskMenuActor, + (state) => state.context.operationContext, + ); + + const nodes = useSelector( + richAddTaskMenuActor, + (state) => state.context.nodes, + ) as NodeData[]; + + const taskRefNames: string[] = useMemo( + () => nodes.map((node) => node?.data?.task?.taskReferenceName), + [nodes], + ); + + const hoveredItem = useSelector( + richAddTaskMenuActor, + (state) => state.context.hoveredItem, + ); + + const setHoveredItem = (data: string) => { + richAddTaskMenuActor.send({ + type: RichAddTaskMenuEventTypes.SET_HOVERED_ITEM, + data, + }); + }; + + const getSequentialTask = useCallback( + ({ + handler, + overrides, + }: { + handler: any; + overrides?: Partial; + }) => { + let newTask = handler({ + overrides, + nameGenerator: noRandomSuffix, + }); + + if (Array.isArray(newTask)) { + newTask = newTask.map((task) => { + const sequentialName = getSequentiallySuffix({ + name: task.taskReferenceName, + refNames: taskRefNames, + }); + + return { + ...task, + name: overrides?.name ? task.name : sequentialName.name, + taskReferenceName: sequentialName.taskReferenceName, + }; + }); + + return newTask; + } + + const sequentialName = getSequentiallySuffix({ + name: newTask.taskReferenceName, + refNames: taskRefNames, + }); + + return { + ...newTask, + name: overrides?.name ? newTask?.name : sequentialName.name, + taskReferenceName: sequentialName.taskReferenceName, + }; + }, + [taskRefNames], + ); + + const handleAddTaskBelow = useCallback( + (payloadGenFn: () => CommonTaskDef | CommonTaskDef[]) => () => { + const dataForOperation = buildDataForOperation( + operationContext?.port, + operationContext?.node, + ); + + onPerformOperation({ + ...dataForOperation, + operation: { + payload: payloadGenFn(), + }, + }); + }, + [onPerformOperation, operationContext], + ); + + const taskOptions = useMemo( + () => + getALL_TASKS().map((bt: BaseTaskMenuItem) => { + const IconComponent = iconForTaskTypeMap[bt.type]; + const generatorForType = taskGeneratorMap[bt.type]; + + const taskRenameMap = (name: string) => { + switch (name) { + case "Event Task": + return "Publish Event"; + case "Inline Task": + return "Javascript"; + case "LLM Chat Complete": + return "Chat Complete"; + case "LLM Index Document": + return "Index Document"; + case "LLM Search Index": + return "Search Document"; + case "LLM Generate Embeddings": + return "Generate Embeddings"; + case "LLM Get Embeddings": + return "Search Embeddings"; + + default: + return name; + } + }; + return { + category: bt.category, + name: taskRenameMap(bt.name), + description: bt.description, + onClick: handleAddTaskBelow(() => + getSequentialTask({ + handler: generatorForType, + }), + ), + type: bt.type, + icon: , + }; + }), + [handleAddTaskBelow, getSequentialTask], + ); + + const workerMenuItems = useSelector( + richAddTaskMenuActor, + (state) => state.context.workerMenuItems ?? [], + ); + + const subWorkflowMenuItems = useSelector( + richAddTaskMenuActor, + (state) => state.context.workflowMenuItems ?? [], + ); + + const workerOptions = useMemo( + () => + workerMenuItems.map((baseItem: BaseTaskMenuItem) => ({ + ...baseItem, + onClick: handleAddTaskBelow(() => + getSequentialTask({ + overrides: { + name: baseItem.name, + taskReferenceName: `${baseItem.name}_ref`, + }, + handler: generateSimpleTask, + }), + ), + })), + [getSequentialTask, handleAddTaskBelow, workerMenuItems], + ); + + const workflowDefinitionsOptions = useMemo( + () => + subWorkflowMenuItems.map((baseItem: BaseTaskMenuItem) => ({ + ...baseItem, + onClick: handleAddTaskBelow(() => + getSequentialTask({ + overrides: { + name: baseItem.name, + taskReferenceName: `${baseItem.name}_ref`, + subWorkflowParam: { + name: baseItem.name, + version: baseItem.version, + }, + }, + handler: generateSubWorkflowTask, + }), + ), + })), + [subWorkflowMenuItems, handleAddTaskBelow, getSequentialTask], + ); + + const options = useMemo( + () => + [ + ...taskOptions, + ...workerOptions, + ...workflowDefinitionsOptions, + ] as TaskMenuItem[], + [taskOptions, workerOptions, workflowDefinitionsOptions], + ); + + const quickTasksOptions = useMemo(() => { + // Build quick-add types: OSS core types + plugin-registered types with quickAdd: true + const pluginQuickAddTypes = pluginRegistry + .getTaskMenuItems() + .filter((item) => item.quickAdd) + .map((item) => item.type as TaskType); + + // Combine OSS and plugin types, but exclude AI tasks (they go in Agentic Orchestration) + const aiTaskTypesSet = new Set(AI_QUICK_ADD_TYPES as string[]); + const coreTaskTypes = [ + ...OSS_QUICK_ADD_TYPES, + ...pluginQuickAddTypes, + ].filter((type) => !aiTaskTypesSet.has(type)); + + // Map through coreTaskTypes to preserve order and find matching tasks + const coreTasks = coreTaskTypes + ?.map((taskType) => taskOptions.find((task) => task.type === taskType)) + ?.filter((task): task is NonNullable => task !== undefined); + + // Map AI tasks for the Agentic Orchestration section + const aiTasks = AI_QUICK_ADD_TYPES.map((taskType) => + taskOptions.find((task) => task.type === taskType), + )?.filter((task): task is NonNullable => task !== undefined); + + // Build final list: + // - Core tasks (no section title) + // - AI tasks with "Agentic Orchestration" divider + const result = [...coreTasks, ...aiTasks]; + + // Store AI section start index for divider rendering + (result as any).__aiStartIndex = coreTasks.length; + + return result; + }, [taskOptions]); + + const filteredOptions = useMemo(() => { + if (options) { + const filterer = itemFilterMatcher(searchQuery, RichAddMenuTabs.ALL_TAB); + return options.filter(filterer); + } else return []; + }, [searchQuery, options]); + + const isFetching = useSelector( + richAddTaskMenuActor, + (state) => + state.matches(`init.main.${MainStates.FETCH_FOR_TASK_DEFINITIONS}`) || + state.matches(`init.main.${MainStates.FETCH_FOR_WORKFLOW_DEFINITIONS}`), + ); + + const [{ menuType }, { handleChangeMenuType }] = + useRichAddTaskMenu(richAddTaskMenuActor); + + const handleTaskClick = (task: TaskMenuItem) => { + if (task.category === RichAddMenuTabs.INTEGRATIONS_TAB) { + handleChangeMenuType("advanced"); + } else if (task.onClick) { + task.onClick(); + } + }; + + const { inputProps, optionPropsForItem } = useArrowNavigation({ + onSelect: (elem) => { + if (elem?.onClick) { + elem?.onClick(); + } + }, + options: filteredOptions.slice(0, 3) || [], + optionsIdGen: uniqueTaskIdGenerator, + scrollToCenter: true, + hoveredItem, + setHoveredItem, + }); + + return ( + + + + {/* Search Header */} + + + + handleSearchChange(e.target.value)} + autoFocus + sx={{ + flex: 1, + fontSize: "0.9375rem", + color: "#1E293B", + "& input": { + padding: 0, + "&::placeholder": { + color: "#94A3B8", + opacity: 1, + }, + }, + }} + endAdornment={ + searchQuery ? ( + handleSearchChange("")} + sx={{ + color: "#94A3B8", + p: 0.5, + "&:hover": { + color: "#64748B", + backgroundColor: "transparent", + }, + }} + > + + + ) : null + } + /> + + + + {/* Quick Add Section */} + {!searchQuery ? ( + + + + QUICK ADD + + + + + {/* Grid Layout: 5 columns × 3 rows = 15 items max */} + + {quickTasksOptions.slice(0, 15).map((item, index) => { + const aiStartIndex = + (quickTasksOptions as any).__aiStartIndex ?? 15; + const showDivider = + index === aiStartIndex && aiStartIndex < 15; + + return ( + + {showDivider && ( + + + + Agentic Orchestration + + + + )} + + + + + + {item.icon} + + {item.name} + + + + + + + ); + })} + + + ) : ( + // Search Results Section + + + + SEARCH RESULTS + + + {filteredOptions.length > 3 && ( + handleChangeMenuType("advanced")} + sx={{ + display: "flex", + alignItems: "center", + gap: 0.75, + px: 1.5, + py: 0.75, + borderRadius: 1.5, + backgroundColor: alpha("#F1F5F9", 0.6), + color: "#64748B", + fontSize: "0.75rem", + fontWeight: 500, + cursor: "pointer", + transition: "all 0.2s ease", + "&:hover": { + backgroundColor: alpha("#3B82F6", 0.08), + color: "#3B82F6", + }, + }} + > + + {isFetching ? ( + + ) : ( + `+${filteredOptions.length - 3} more` + )} + + + )} + + + {isFetching && filteredOptions.length === 0 ? ( + + + + ) : filteredOptions.length === 0 ? ( + + + + No tasks found + + + ) : ( + <> + {filteredOptions.slice(0, 3).map((item, index) => ( + handleTaskClick(item)} + sx={{ + p: 2, + borderRadius: 2, + cursor: "pointer", + backgroundColor: + uniqueTaskIdGenerator(item) === hoveredItem + ? alpha("#F1F5F9", 0.6) + : "#FFFFFF", + border: "2px solid", + borderColor: + uniqueTaskIdGenerator(item) === hoveredItem + ? alpha("#3B82F6", 0.2) + : "transparent", + transition: "all 0.2s ease", + "&:hover": { + backgroundColor: alpha("#F1F5F9", 0.6), + borderColor: alpha("#3B82F6", 0.2), + transform: "translateY(-1px)", + "& .task-icon-box": { + backgroundColor: alpha("#3B82F6", 0.08), + }, + }, + }} + > + + + {item?.icon ? ( + cloneElement(item.icon, { + color: + uniqueTaskIdGenerator(item) === hoveredItem + ? "#3B82F6" + : "#64748B", + }) + ) : ( + + {getInitials(item.name)} + + )} + + + + {item.name} + + + {item.description} + + {item.category === + RichAddMenuTabs.INTEGRATIONS_TAB && ( + + {item.status === "active" ? ( + + ) : ( + + )} + {item.status === "active" + ? "Ready to use" + : "Setup required"} + + )} + + + + ))} + {isFetching && filteredOptions.length < 3 && ( + + + + )} + + )} + + + )} + + + + ); +}; + +export default QuickAddMenu; diff --git a/ui-next/src/components/flow/components/RichAddTaskMenu/helpers.ts b/ui-next/src/components/flow/components/RichAddTaskMenu/helpers.ts new file mode 100644 index 0000000000..b67a7e5824 --- /dev/null +++ b/ui-next/src/components/flow/components/RichAddTaskMenu/helpers.ts @@ -0,0 +1,105 @@ +import { BaseTaskMenuItem, RichAddMenuTabs } from "./state/types"; + +export const itemMatchesSelectedTask = ( + item: BaseTaskMenuItem, + selectedTab: RichAddMenuTabs, +) => selectedTab === RichAddMenuTabs.ALL_TAB || selectedTab === item.category; + +export const itemNameIncludesText = ( + item: BaseTaskMenuItem, + searchQuery: string, +) => { + const query = searchQuery?.toLowerCase(); + return ( + item?.name?.toLowerCase()?.includes(query) || + item?.type?.toLowerCase()?.includes(query) + ); +}; + +export const itemFilterMatcher = + (searchQuery: string, selectedTab: RichAddMenuTabs) => + (item: BaseTaskMenuItem) => + itemMatchesSelectedTask(item, selectedTab) && + itemNameIncludesText(item, searchQuery); + +interface JSONSchemaProperty { + type: string; + properties?: Record; + items?: JSONSchemaProperty; + required?: string[]; + enum?: any[]; + default?: any; + minimum?: number; + maximum?: number; + description?: string; + additionalProperties?: boolean; + $schema?: string; +} + +interface JSONSchema extends JSONSchemaProperty { + type: string; + properties?: Record; + required?: string[]; +} + +export const generateObjectFromSchema = (schema: JSONSchema): any => { + if (!schema?.type) { + return undefined; + } + + switch (schema?.type) { + case "object": { + if (!schema.properties) { + return {}; + } + + const obj: Record = {}; + Object.entries(schema?.properties || {}).forEach(([key, prop]) => { + if (prop?.default !== undefined) { + obj[key] = prop?.default; + } else if (prop?.enum && prop?.enum?.length > 0) { + obj[key] = prop?.enum[0]; + } else if (prop?.type === "integer" || prop?.type === "number") { + // Handle minimum/maximum constraints + if (prop?.minimum !== undefined) { + obj[key] = prop?.minimum; + } else if (prop?.maximum !== undefined) { + obj[key] = Math.min( + prop?.maximum, + prop?.type === "integer" ? 1 : 1.0, + ); + } else { + obj[key] = prop?.type === "integer" ? 1 : 1.0; + } + } else { + obj[key] = generateObjectFromSchema(prop as JSONSchema); + } + }); + return obj; + } + case "array": { + if (!schema?.items) { + return []; + } + return [generateObjectFromSchema(schema?.items as JSONSchema)]; + } + case "string": { + return ""; + } + case "integer": { + return schema?.minimum !== undefined ? schema?.minimum : 1; + } + case "number": { + return schema?.minimum !== undefined ? schema?.minimum : 1.0; + } + case "boolean": { + return false; + } + case "null": { + return null; + } + default: { + return undefined; + } + } +}; diff --git a/ui-next/src/components/flow/components/RichAddTaskMenu/iconsForTaskTypes.ts b/ui-next/src/components/flow/components/RichAddTaskMenu/iconsForTaskTypes.ts new file mode 100644 index 0000000000..77d2ff8d0f --- /dev/null +++ b/ui-next/src/components/flow/components/RichAddTaskMenu/iconsForTaskTypes.ts @@ -0,0 +1,97 @@ +import { TaskType } from "types"; +import { + Cards, + CloudArrowDown, + Function, + Hourglass, + Person as HumanTaskIcon, + Pause, + Repeat, + RocketLaunch, + ShieldCheck, + X, + Diamond, + GitFork, + Globe, + FileJsIcon, + HandshakeIcon, + ClockClockwiseIcon, + PersonSimpleRunIcon, + BroadcastIcon, + RowsIcon, + FilesIcon, + FileMagnifyingGlass, +} from "@phosphor-icons/react"; +import SendgridIcon from "../shapes/TaskCard/icons/Sendgrid"; + +import HttpPollIcon from "../shapes/TaskCard/icons/HttpPoll"; +import JsonIcon from "../shapes/TaskCard/icons/Json"; + +import WorkerSimpleIcon from "../shapes/TaskCard/icons/Worker"; +import SimpleWorkerIcon from "../shapes/TaskCard/icons/Simple"; +import LlmTextComplete from "../shapes/TaskCard/icons/LlmTextComplete"; +import LlmGenerateEmbeddings from "../shapes/TaskCard/icons/LlmGenerateEmbeddings"; +import LlmGetEmbeddings from "../shapes/TaskCard/icons/LlmGetEmbeddings"; +import LlmStoreEmbeddings from "../shapes/TaskCard/icons/LlmStoreEmbeddings"; +import LlmSearchIndex from "../shapes/TaskCard/icons/LlmSearchIndex"; +import LlmIndexDocument from "../shapes/TaskCard/icons/LlmIndexDocument"; +import GetDocument from "../shapes/TaskCard/icons/GetDocument"; +import LlmIndexText from "../shapes/TaskCard/icons/LlmIndexText"; +import QueryProcessor from "../shapes/TaskCard/icons/QueryProcessor"; +import OpsGenie from "../shapes/TaskCard/icons/OpsGenie"; +import UpdateTaskIcon from "../shapes/TaskCard/icons/UpdateTaskIcon"; +import UpdateSecretIcon from "../shapes/TaskCard/icons/UpdateSecret"; +import LlmChatComplete from "../shapes/TaskCard/icons/LlmChatComplete"; + +import { FormTaskType } from "types"; +import React from "react"; +import MCPIcon from "../shapes/TaskCard/icons/MCPIcon"; +import { ForkJoinIcon } from "../shapes/TaskCard/icons/ForkJoinIcon"; + +export const iconForTaskTypeMap = { + [TaskType.WAIT]: Hourglass, + [TaskType.HTTP]: Globe, + [TaskType.KAFKA_PUBLISH]: WorkerSimpleIcon, // This one is not really used + [TaskType.HUMAN]: HumanTaskIcon, + [TaskType.BUSINESS_RULE]: HandshakeIcon, + [TaskType.SENDGRID]: SendgridIcon, + [TaskType.WAIT_FOR_WEBHOOK]: ClockClockwiseIcon, + [TaskType.HTTP_POLL]: HttpPollIcon, + [TaskType.DO_WHILE]: Repeat, + [TaskType.SIMPLE]: SimpleWorkerIcon, + [TaskType.YIELD]: Pause, + [TaskType.JDBC]: PersonSimpleRunIcon, // Would be great if it had a good icon + [TaskType.EVENT]: BroadcastIcon, + [TaskType.JOIN]: GitFork, + [TaskType.FORK_JOIN]: ForkJoinIcon, + [TaskType.FORK_JOIN_DYNAMIC]: ForkJoinIcon, + [TaskType.DYNAMIC]: Cards, + [TaskType.INLINE]: FileJsIcon, + [TaskType.SWITCH]: Diamond, + [TaskType.JSON_JQ_TRANSFORM]: JsonIcon, + [TaskType.TERMINATE]: X, + [TaskType.SET_VARIABLE]: Function, + [TaskType.TERMINATE_WORKFLOW]: X, + [TaskType.SUB_WORKFLOW]: ForkJoinIcon, + [TaskType.START_WORKFLOW]: RocketLaunch, + [TaskType.LLM_TEXT_COMPLETE]: LlmTextComplete, + [TaskType.LLM_GENERATE_EMBEDDINGS]: LlmGenerateEmbeddings, + [TaskType.LLM_GET_EMBEDDINGS]: LlmGetEmbeddings, + [TaskType.LLM_STORE_EMBEDDINGS]: LlmStoreEmbeddings, + [TaskType.LLM_INDEX_DOCUMENT]: LlmIndexDocument, + [TaskType.LLM_SEARCH_INDEX]: LlmSearchIndex, + [TaskType.LLM_INDEX_TEXT]: LlmIndexText, + [TaskType.UPDATE_SECRET]: UpdateSecretIcon, + [TaskType.GET_DOCUMENT]: GetDocument, + [TaskType.QUERY_PROCESSOR]: QueryProcessor, + [TaskType.OPS_GENIE]: OpsGenie, + [TaskType.GET_SIGNED_JWT]: ShieldCheck, + [TaskType.UPDATE_TASK]: UpdateTaskIcon, + [TaskType.GET_WORKFLOW]: CloudArrowDown, + [TaskType.LLM_CHAT_COMPLETE]: LlmChatComplete, + [TaskType.GRPC]: Globe, + [TaskType.MCP]: MCPIcon, + [TaskType.CHUNK_TEXT]: RowsIcon, + [TaskType.LIST_FILES]: FilesIcon, + [TaskType.PARSE_DOCUMENT]: FileMagnifyingGlass, +} satisfies Record; diff --git a/ui-next/src/components/flow/components/RichAddTaskMenu/index.ts b/ui-next/src/components/flow/components/RichAddTaskMenu/index.ts new file mode 100644 index 0000000000..72ddf2c223 --- /dev/null +++ b/ui-next/src/components/flow/components/RichAddTaskMenu/index.ts @@ -0,0 +1,2 @@ +export * from "./state"; +export * from "./supportedTasks"; diff --git a/ui-next/src/components/flow/components/RichAddTaskMenu/state/actions.ts b/ui-next/src/components/flow/components/RichAddTaskMenu/state/actions.ts new file mode 100644 index 0000000000..440b93bb8f --- /dev/null +++ b/ui-next/src/components/flow/components/RichAddTaskMenu/state/actions.ts @@ -0,0 +1,275 @@ +import { WorkflowDef } from "types/WorkflowDef"; +import { assign, DoneInvokeEvent, raise } from "xstate"; +import { + RichAddTaskMenuMachineContext, + TypingEvent, + TaskDefinition, + SetHoveredItemEvent, + SetSelectedTabEvent, + RichAddMenuTabs, + GotToEndEvent, + BaseTaskMenuItem, + RichAddTaskMenuEventTypes, + ScrollToTopEvent, + SetMenuTypeEvent, + IntegrationMenuItem, + FetchIntegrationToolsEvent, + UpdateIntegrationDrillDownEvent, +} from "./types"; +import { TaskType } from "types/common"; +import _first from "lodash/first"; +import { itemFilterMatcher } from "../helpers"; +import { uniqueTaskIdGenerator } from "../taskGenerator"; + +// Type for raw integration data from API +interface RawIntegrationData { + name: string; + description?: string; + type: string; + iconName?: string; + category?: string; +} + +export const persistSearchQuery = assign< + RichAddTaskMenuMachineContext, + TypingEvent +>({ + searchQuery: (context, event) => event.text, +}); + +export const persistTaskDefinitions = assign< + RichAddTaskMenuMachineContext, + DoneInvokeEvent +>((_context, event) => { + return { + taskDefinitions: event.data, + workerMenuItems: event.data.map( + (task): BaseTaskMenuItem => ({ + category: RichAddMenuTabs.WORKERS_TAB, + name: task.name, + description: task.description, + type: TaskType.SIMPLE, + }), + ), + isTaskDefFetched: true, // FIXME, remove flags. + }; +}); + +export const persistWorkflowDefinitions = assign< + RichAddTaskMenuMachineContext, + DoneInvokeEvent +>((_context, event) => { + return { + workflowDefinitions: event.data, + workflowMenuItems: event.data.map( + (workflow): BaseTaskMenuItem => ({ + category: RichAddMenuTabs.SUB_WORKFLOWS_TAB, + name: workflow.name, + description: workflow.description, + version: workflow.version, + type: TaskType.SUB_WORKFLOW, + }), + ), + isSubWfFetched: true, + }; +}); + +export const persistIntegrations = assign< + RichAddTaskMenuMachineContext, + DoneInvokeEvent +>((context, event) => { + const data = event.data as + | { + supportedIntegrations?: RawIntegrationData[]; + availableIntegrations?: RawIntegrationData[]; + } + | undefined; + + // If the fetch failed or returned bad data, preserve existing integrations + if (!data || (!data.supportedIntegrations && !data.availableIntegrations)) { + console.warn( + "[persistIntegrations] No integration data received, preserving existing state", + ); + return { + isIntegrationsFetched: true, + }; + } + + // Get the types that are already in availableIntegrations + const availableIntegrationTypes = + data?.availableIntegrations?.map((integration) => integration.type) || []; + + return { + integrationDefs: + (data?.supportedIntegrations as never) ?? context.integrationDefs, + supportedIntegrations: + data?.supportedIntegrations?.map( + (integration): IntegrationMenuItem => ({ + category: RichAddMenuTabs.INTEGRATIONS_TAB, + name: integration.name, + description: integration.description ?? "", + type: TaskType.MCP, + integrationType: integration.type, + iconName: integration.iconName ?? "", + status: availableIntegrationTypes.includes(integration.type) + ? "active" + : "inactive", + }), + ) ?? + context.supportedIntegrations ?? + [], + availableIntegrations: + data?.availableIntegrations?.map( + (integration): IntegrationMenuItem => ({ + category: RichAddMenuTabs.INTEGRATIONS_TAB, + name: integration.name, + description: integration.description ?? "", + type: TaskType.MCP, + integrationType: integration.type, + iconName: + data?.supportedIntegrations?.find( + (supportedIntegration) => + supportedIntegration.type === integration.type, + )?.iconName ?? "", + status: "active", + }), + ) ?? + context.availableIntegrations ?? + [], + isIntegrationsFetched: true, + }; +}); + +export const persistHoveredItem = assign< + RichAddTaskMenuMachineContext, + SetHoveredItemEvent +>({ + hoveredItem: (context, event) => event.data, +}); + +export const persistSelectedTab = assign< + RichAddTaskMenuMachineContext, + SetSelectedTabEvent +>({ + selectedTab: (context, event) => event.tab, +}); + +export const setSelectedTabAll = assign({ + selectedTab: RichAddMenuTabs.ALL_TAB, +}); + +export const persistLastScrollPosition = assign< + RichAddTaskMenuMachineContext, + GotToEndEvent +>({ + lastScrollTopPosition: (context, event) => event.lastScrollTopPosition, +}); + +export const updateToScrollTop = assign({ + toScrollTop: (context) => context.lastScrollTopPosition, +}); + +export const persistToScrollTop = assign({ + toScrollTop: () => 0, + lastScrollTopPosition: () => 0, +}); + +export const resetToScrollTop = raise< + RichAddTaskMenuMachineContext, + ScrollToTopEvent +>( + (__, _event) => ({ + type: RichAddTaskMenuEventTypes.SCROLL_TO_TOP, + }), + { delay: 100 }, +); + +export const preSelectTheFirstItem = assign< + RichAddTaskMenuMachineContext, + SetSelectedTabEvent +>({ + hoveredItem: (context) => { + const allItems = context.baseTaskMenuItems + .concat(context?.workerMenuItems ?? []) + .concat(context?.workflowMenuItems); + + const finder = itemFilterMatcher(context.searchQuery, context.selectedTab); + + const firstItem = allItems.find(finder); + + return firstItem ? uniqueTaskIdGenerator(firstItem) : context.hoveredItem; + }, +}); +export const hoverFirstItem = assign({ + hoveredItem: (context) => { + const firstAllTasks = _first(context.baseTaskMenuItems); + return firstAllTasks + ? uniqueTaskIdGenerator(firstAllTasks) + : context.hoveredItem; + }, +}); + +export const persistMenuType = assign< + RichAddTaskMenuMachineContext, + SetMenuTypeEvent +>({ + menuType: (context, event) => event.menuType, +}); + +export const persistSelectedIntegration = assign< + RichAddTaskMenuMachineContext, + FetchIntegrationToolsEvent +>({ + integrationDrillDownMenu: (context, event) => ({ + ...context.integrationDrillDownMenu, + selectedIntegration: event.integration, + }), +}); + +// export const clearSelectedIntegration = assign< +// RichAddTaskMenuMachineContext, +// SetSelectedTabEvent +// >({ +// selectedIntegration: (_context) => undefined, +// }); + +export const persistSelectedIntegrationTools = assign< + RichAddTaskMenuMachineContext, + DoneInvokeEvent[]> +>({ + integrationDrillDownMenu: (context, event) => ({ + ...context.integrationDrillDownMenu, + level: "tools", + selectedIntegrationTools: event.data, + }), +}); + +// export const clearSelectedIntegrationTools = assign< +// RichAddTaskMenuMachineContext, +// SetSelectedTabEvent +// >({ +// selectedIntegrationTools: (_context) => undefined, +// }); + +export const persistIntegrationDrillDown = assign< + RichAddTaskMenuMachineContext, + UpdateIntegrationDrillDownEvent +>({ + integrationDrillDownMenu: (_context, event) => event.data, +}); + +export const switchToAdvancedMenu = raise< + RichAddTaskMenuMachineContext, + SetMenuTypeEvent +>({ + type: RichAddTaskMenuEventTypes.SET_MENU_TYPE, + menuType: "advanced", +}); + +export const switchSelectedTabToIntegrations = raise< + RichAddTaskMenuMachineContext, + SetSelectedTabEvent +>({ + type: RichAddTaskMenuEventTypes.SET_SELECTED_TAB, + tab: RichAddMenuTabs.INTEGRATIONS_TAB, +}); diff --git a/ui-next/src/components/flow/components/RichAddTaskMenu/state/guards.ts b/ui-next/src/components/flow/components/RichAddTaskMenu/state/guards.ts new file mode 100644 index 0000000000..67041a5c7a --- /dev/null +++ b/ui-next/src/components/flow/components/RichAddTaskMenu/state/guards.ts @@ -0,0 +1,34 @@ +import { + RichAddMenuTabs, + RichAddTaskMenuMachineContext, + SetSelectedTabEvent, +} from "./types"; + +export const isTabIsWorkers = ( + _context: RichAddTaskMenuMachineContext, + { tab }: SetSelectedTabEvent, +) => { + return tab === RichAddMenuTabs.WORKERS_TAB; +}; + +export const isTabIsSubWorkflows = ( + _context: RichAddTaskMenuMachineContext, + { tab }: SetSelectedTabEvent, +) => tab === RichAddMenuTabs.SUB_WORKFLOWS_TAB; + +export const isTaskDefNotFetched = ({ + isTaskDefFetched, +}: RichAddTaskMenuMachineContext) => !isTaskDefFetched; + +export const isSubWfNotFetched = ({ + isSubWfFetched, +}: RichAddTaskMenuMachineContext) => !isSubWfFetched; + +export const isIntegrationsNotFetched = ({ + isIntegrationsFetched, +}: RichAddTaskMenuMachineContext) => !isIntegrationsFetched; + +export const isTabIsIntegrations = ( + _context: RichAddTaskMenuMachineContext, + { tab }: SetSelectedTabEvent, +) => tab === RichAddMenuTabs.INTEGRATIONS_TAB; diff --git a/ui-next/src/components/flow/components/RichAddTaskMenu/state/hook.ts b/ui-next/src/components/flow/components/RichAddTaskMenu/state/hook.ts new file mode 100644 index 0000000000..f795eba038 --- /dev/null +++ b/ui-next/src/components/flow/components/RichAddTaskMenu/state/hook.ts @@ -0,0 +1,158 @@ +import { useSelector } from "@xstate/react"; +import { ActorRef } from "xstate"; + +import { + IntegrationDrillDownMenuProp, + IntegrationMenuItem, + MainStates, + RichAddTaskMenuEvents, + RichAddTaskMenuEventTypes, +} from "./types"; +import { NodeData } from "reaflow"; + +export const useRichAddTaskMenu = ( + richAddTaskMenuActor: ActorRef, +) => { + const menuType = useSelector( + richAddTaskMenuActor, + (state) => state.context.menuType, + ); + + const supportedIntegrations = useSelector( + richAddTaskMenuActor, + (state) => state.context.supportedIntegrations, + ); + + const availableIntegrations = useSelector( + richAddTaskMenuActor, + (state) => state.context.availableIntegrations, + ); + + const integrationDefs = useSelector( + richAddTaskMenuActor, + (state) => state.context.integrationDefs, + ); + + const integrationTypes = useSelector( + richAddTaskMenuActor, + (state) => state.context.integrationTypes, + ); + + const integrationDrillDownMenu = useSelector( + richAddTaskMenuActor, + (state) => state.context?.integrationDrillDownMenu, + ); + + // Add scroll position tracking + const scrollPosition = useSelector( + richAddTaskMenuActor, + (state) => state.context.toScrollTop, + ); + + const operationContext = useSelector( + richAddTaskMenuActor, + (state) => state.context.operationContext, + ); + + const nodes = useSelector( + richAddTaskMenuActor, + (state) => state.context.nodes, + ) as NodeData[]; + + const workerMenuItems = useSelector( + richAddTaskMenuActor, + (state) => state.context.workerMenuItems ?? [], + ); + + const subWorkflowMenuItems = useSelector( + richAddTaskMenuActor, + (state) => state.context.workflowMenuItems ?? [], + ); + + const selectedTab = useSelector( + richAddTaskMenuActor, + (state) => state.context.selectedTab, + ); + + const isFetching = useSelector( + richAddTaskMenuActor, + (state) => + state.matches(`init.main.${MainStates.FETCH_FOR_TASK_DEFINITIONS}`) || + state.matches(`init.main.${MainStates.FETCH_FOR_WORKFLOW_DEFINITIONS}`) || + state.matches(`init.main.${MainStates.FETCH_FOR_INTEGRATIONS}`), + ); + + const isFetchingIntegrationTools = useSelector( + richAddTaskMenuActor, + (state) => + state.matches(`init.main.${MainStates.FETCH_FOR_INTEGRATION_TOOLS}`), + ); + + const searchQuery = useSelector( + richAddTaskMenuActor, + (state) => state.context.searchQuery, + ); + + const handleChangeMenuType = (menuType: "quick" | "advanced") => { + richAddTaskMenuActor.send({ + type: RichAddTaskMenuEventTypes.SET_MENU_TYPE, + menuType, + }); + }; + + const handleFetchIntegrationTools = (integration: IntegrationMenuItem) => { + richAddTaskMenuActor.send({ + type: RichAddTaskMenuEventTypes.FETCH_INTEGRATION_TOOLS, + integration, + }); + }; + + const refetchIntegrations = () => { + richAddTaskMenuActor.send({ + type: RichAddTaskMenuEventTypes.REFETCH_INTEGRATIONS, + }); + }; + + const handleUpdateIntegrationDrillDown = ( + integration: IntegrationDrillDownMenuProp, + ) => { + richAddTaskMenuActor.send({ + type: RichAddTaskMenuEventTypes.UPDATE_INTEGRATION_DRILL_DOWN, + data: integration, + }); + }; + + const handleTyping = (value: any) => { + richAddTaskMenuActor.send({ + type: RichAddTaskMenuEventTypes.TYPING, + text: value, + }); + }; + + return [ + { + menuType, + supportedIntegrations: supportedIntegrations ?? [], + availableIntegrations: availableIntegrations ?? [], + integrationDefs: integrationDefs ?? [], + integrationTypes: integrationTypes ?? [], + integrationDrillDownMenu: integrationDrillDownMenu ?? {}, + scrollPosition, + operationContext, + nodes, + workerMenuItems, + subWorkflowMenuItems, + selectedTab, + isFetching, + isFetchingIntegrationTools, + searchQuery, + }, + { + handleChangeMenuType, + handleFetchIntegrationTools, + refetchIntegrations, + handleUpdateIntegrationDrillDown, + handleTyping, + }, + ] as const; +}; diff --git a/ui-next/src/components/flow/components/RichAddTaskMenu/state/index.ts b/ui-next/src/components/flow/components/RichAddTaskMenu/state/index.ts new file mode 100644 index 0000000000..53401e46b9 --- /dev/null +++ b/ui-next/src/components/flow/components/RichAddTaskMenu/state/index.ts @@ -0,0 +1,2 @@ +export * from "./types"; +export * from "./machine"; diff --git a/ui-next/src/components/flow/components/RichAddTaskMenu/state/machine.ts b/ui-next/src/components/flow/components/RichAddTaskMenu/state/machine.ts new file mode 100644 index 0000000000..01c2899687 --- /dev/null +++ b/ui-next/src/components/flow/components/RichAddTaskMenu/state/machine.ts @@ -0,0 +1,293 @@ +import { createMachine, sendParent } from "xstate"; +import { + RichAddTaskMenuMachineContext, + MainStates, + RichAddTaskMenuEventTypes, + RichAddTaskMenuEvents, + RichAddMenuTabs, +} from "./types"; +import * as actions from "./actions"; +import * as services from "./services"; +import * as guards from "./guards"; + +const MINIMUM_CHARS_TO_SEARCH = 3; + +export const richAddTaskMenuMachine = createMachine< + RichAddTaskMenuMachineContext, + RichAddTaskMenuEvents +>( + { + id: "richAddTaskMenuMachine", + initial: "init", + predictableActionArguments: true, + context: { + taskDefinitions: [], + workflowDefinitions: [], + authHeaders: undefined, + operationContext: undefined, + searchQuery: "", + nodes: [], + hoveredItem: "", + selectedTab: RichAddMenuTabs.ALL_TAB, + isSubWfFetched: false, + isTaskDefFetched: false, + isIntegrationsFetched: false, + lastScrollTopPosition: 0, + toScrollTop: 0, + baseTaskMenuItems: [], + workerMenuItems: [], + workflowMenuItems: [], + supportedIntegrations: [], + availableIntegrations: [], + menuType: "quick", + integrationDrillDownMenu: { + isOpen: false, + selectedIntegration: null, + selectedRootIntegration: null, + level: "integrations", + }, + }, + + states: { + init: { + type: "parallel", + states: { + main: { + initial: MainStates.INIT, + on: { + [RichAddTaskMenuEventTypes.CLOSE_MENU]: { + actions: ["setSelectedTabAll"], + target: `#richAddTaskMenuMachine.init.main.${MainStates.CLOSED}`, + }, + [RichAddTaskMenuEventTypes.SET_HOVERED_ITEM]: { + actions: ["persistHoveredItem"], + }, + [RichAddTaskMenuEventTypes.SET_SELECTED_TAB]: [ + { + cond: (context, event) => + guards.isTabIsWorkers(context, event) && + guards.isTaskDefNotFetched(context), + actions: ["persistSelectedTab"], + target: `#richAddTaskMenuMachine.init.main.${MainStates.FETCH_FOR_TASK_DEFINITIONS}`, + }, + + { + cond: (context, event) => + guards.isTabIsIntegrations(context, event) && + guards.isIntegrationsNotFetched(context), + actions: ["persistSelectedTab"], + target: `#richAddTaskMenuMachine.init.main.${MainStates.FETCH_FOR_INTEGRATIONS}`, + }, + { + actions: [ + "persistSelectedTab", + "preSelectTheFirstItem", + "persistToScrollTop", + ], + }, + ], + [RichAddTaskMenuEventTypes.SET_MENU_TYPE]: [ + { + cond: (_, event) => event.menuType === "advanced", + actions: ["persistMenuType", sendParent((_, event) => event)], + }, + { + actions: ["persistMenuType"], + }, + ], + [RichAddTaskMenuEventTypes.SWITCH_TO_INTEGRATIONS]: { + actions: [ + "switchToAdvancedMenu", + "switchSelectedTabToIntegrations", + ], + }, + }, + states: { + [MainStates.INIT]: { + entry: "hoverFirstItem", + always: { + target: MainStates.IDLE, + }, + }, + [MainStates.CLOSED]: { + always: { + target: "#richAddTaskMenuMachine.final", + }, + }, + [MainStates.IDLE]: { + after: { + 500: { + target: MainStates.FETCH_FOR_TASK_DEFINITIONS, + cond: (context) => { + const result = + context.searchQuery.length > MINIMUM_CHARS_TO_SEARCH; + return result; + }, + }, + }, + on: { + [RichAddTaskMenuEventTypes.TYPING]: { + target: MainStates.IDLE, + actions: "preSelectTheFirstItem", + }, + [RichAddTaskMenuEventTypes.GOT_TO_END]: { + actions: "persistLastScrollPosition", + target: "fetchForTaskDefinitions", + }, + }, + }, + [MainStates.FETCH_FOR_TASK_DEFINITIONS]: { + invoke: { + id: "fetchForTaskDefinitions", + src: "fetchForTaskDefinitions", + onDone: { + actions: "persistTaskDefinitions", + target: MainStates.WITH_TASK_DEFINITIONS, + }, + }, + }, + + [MainStates.FETCH_FOR_INTEGRATIONS]: { + invoke: { + id: "fetchForMCPIntegrations", + src: "fetchForMCPIntegrations", + onDone: { + actions: "persistIntegrations", + target: MainStates.WITH_INTEGRATIONS, + }, + onError: { + // On error, still transition to WITH_INTEGRATIONS but keep existing data + target: MainStates.WITH_INTEGRATIONS, + }, + }, + }, + [MainStates.WITH_TASK_DEFINITIONS]: { + after: { + 500: { + target: MainStates.FETCH_FOR_INTEGRATIONS, + cond: (context) => + context.searchQuery.length > MINIMUM_CHARS_TO_SEARCH + 1, // test event type + }, + }, + entry: ["updateToScrollTop", "preSelectTheFirstItem"], + on: { + [RichAddTaskMenuEventTypes.SET_SELECTED_TAB]: [ + { + cond: (context, event) => + guards.isTabIsIntegrations(context, event) && + !guards.isIntegrationsNotFetched(context), + target: MainStates.WITH_INTEGRATIONS, + actions: ["persistSelectedTab"], + }, + ], + [RichAddTaskMenuEventTypes.TYPING]: { + target: MainStates.WITH_TASK_DEFINITIONS, + }, + [RichAddTaskMenuEventTypes.GOT_TO_END]: { + target: MainStates.FETCH_FOR_INTEGRATIONS, + actions: "persistLastScrollPosition", + }, + }, + }, + [MainStates.WITH_WORKFLOW_DEFINITIONS]: { + after: { + 500: { + target: MainStates.FETCH_FOR_INTEGRATIONS, + cond: (context) => + context.searchQuery.length > MINIMUM_CHARS_TO_SEARCH + 1, + }, + }, + entry: ["updateToScrollTop", "preSelectTheFirstItem"], + on: { + [RichAddTaskMenuEventTypes.SET_SELECTED_TAB]: [ + { + cond: (context, event) => + guards.isTabIsWorkers(context, event) && + guards.isTaskDefNotFetched(context), + target: MainStates.FETCH_FOR_TASK_DEFINITIONS, + actions: ["persistSelectedTab"], + }, + { + cond: (context, event) => + guards.isTabIsIntegrations(context, event) && + guards.isIntegrationsNotFetched(context), + target: MainStates.FETCH_FOR_INTEGRATIONS, + actions: ["persistSelectedTab"], + }, + { + cond: (context, event) => + guards.isTabIsIntegrations(context, event) && + !guards.isIntegrationsNotFetched(context), + target: MainStates.WITH_INTEGRATIONS, + actions: ["persistSelectedTab"], + }, + ], + [RichAddTaskMenuEventTypes.GOT_TO_END]: { + target: MainStates.FETCH_FOR_INTEGRATIONS, + actions: "persistLastScrollPosition", + }, + }, + }, + [MainStates.WITH_INTEGRATIONS]: { + // Nothing to do here we rendered everything. + entry: ["updateToScrollTop", "preSelectTheFirstItem"], + on: { + [RichAddTaskMenuEventTypes.SET_SELECTED_TAB]: [ + { + cond: (context, event) => + guards.isTabIsWorkers(context, event) && + guards.isTaskDefNotFetched(context), + target: MainStates.FETCH_FOR_TASK_DEFINITIONS, + actions: ["persistSelectedTab"], + }, + ], + [RichAddTaskMenuEventTypes.FETCH_INTEGRATION_TOOLS]: [ + { + actions: ["persistSelectedIntegration"], + target: MainStates.FETCH_FOR_INTEGRATION_TOOLS, + }, + ], + [RichAddTaskMenuEventTypes.UPDATE_INTEGRATION_DRILL_DOWN]: { + actions: ["persistIntegrationDrillDown"], + target: MainStates.WITH_INTEGRATIONS, + }, + [RichAddTaskMenuEventTypes.REFETCH_INTEGRATIONS]: { + target: MainStates.FETCH_FOR_INTEGRATIONS, + }, + }, + }, + [MainStates.FETCH_FOR_INTEGRATION_TOOLS]: { + invoke: { + id: "fetchForIntegrationTools", + src: "fetchForIntegrationTools", + onDone: { + actions: "persistSelectedIntegrationTools", + target: MainStates.WITH_INTEGRATIONS, + }, + onError: { + target: MainStates.WITH_INTEGRATIONS, + }, + }, + }, + }, + }, + [MainStates.SEARCH_FIELD]: { + on: { + [RichAddTaskMenuEventTypes.TYPING]: { + actions: ["persistSearchQuery", "preSelectTheFirstItem"], + }, + }, + }, + }, + }, + final: { + type: "final", + }, + }, + }, + { + services, + actions: actions as any, + guards: guards as any, + }, +); diff --git a/ui-next/src/components/flow/components/RichAddTaskMenu/state/services.ts b/ui-next/src/components/flow/components/RichAddTaskMenu/state/services.ts new file mode 100644 index 0000000000..b0079c75f8 --- /dev/null +++ b/ui-next/src/components/flow/components/RichAddTaskMenu/state/services.ts @@ -0,0 +1,101 @@ +import { queryClient } from "queryClient"; +import { fetchWithContext, fetchContextNonHook } from "plugins/fetch"; +import { logger } from "utils/logger"; + +import { featureFlags, FEATURES } from "utils/flags"; +import { RichAddTaskMenuMachineContext } from "./types"; + +const fetchContext = fetchContextNonHook(); + +const taskVisibility = featureFlags.getValue(FEATURES.TASK_VISIBILITY, "READ"); + +export const fetchForTaskDefinitions = async ({ + authHeaders: headers, +}: RichAddTaskMenuMachineContext) => { + const taskDefinitionsUrl = `/metadata/taskdefs?access=${taskVisibility}`; + + logger.info("Will search for task definitions", taskDefinitionsUrl); + try { + const response = await queryClient.fetchQuery( + [fetchContext.stack, taskDefinitionsUrl], + () => fetchWithContext(taskDefinitionsUrl, fetchContext, { headers }), + ); + return response; + } catch (error) { + logger.error("Fetching task list page", error); + return Promise.reject({ message: "Error fetching task list page" }); + } +}; + +export const fetchForWorkflowDefinitions = async ({ + authHeaders: headers, +}: RichAddTaskMenuMachineContext) => { + const workflowDefinitionUrl = `/metadata/workflow?short=true`; + + logger.info("Will search for workflow definitions", workflowDefinitionUrl); + try { + const response = await queryClient.fetchQuery( + [fetchContext.stack, workflowDefinitionUrl], + () => fetchWithContext(workflowDefinitionUrl, fetchContext, { headers }), + ); + return response; + } catch (error) { + logger.error("Fetching task list page", error); + return Promise.reject({ message: "Error fetching task list page" }); + } +}; + +export const fetchForMCPIntegrations = async ({ + authHeaders: headers, +}: RichAddTaskMenuMachineContext) => { + const integrationsUrl = `/integrations/def`; + const providersUrl = `/integrations/provider?category=MCP&activeOnly=false`; + + try { + const [integrationsResult, providersResult] = await Promise.allSettled([ + queryClient.fetchQuery([fetchContext.stack, integrationsUrl], () => + fetchWithContext(integrationsUrl, fetchContext, { headers }), + ), + queryClient.fetchQuery([fetchContext.stack, providersUrl], () => + fetchWithContext(providersUrl, fetchContext, { headers }), + ), + ]); + + logger.info("Returning integrations and providers", { + integrationsResult, + providersResult, + }); + return { + supportedIntegrations: + integrationsResult.status === "fulfilled" + ? integrationsResult.value?.filter( + (integration: any) => integration.category === "MCP", + ) + : [], + availableIntegrations: + providersResult.status === "fulfilled" ? providersResult.value : [], + }; + } catch (error) { + logger.error("Fetching integrations", error); + return Promise.reject({ message: "Error fetching integrations" }); + } +}; + +export const fetchForIntegrationTools = async ({ + authHeaders: headers, + integrationDrillDownMenu: { selectedIntegration }, +}: RichAddTaskMenuMachineContext) => { + const toolsUrl = `/integrations/${selectedIntegration?.name}/def/apis`; + + logger.info("Will search for integration tools", toolsUrl); + try { + const response = await queryClient.fetchQuery( + [fetchContext.stack, toolsUrl], + () => fetchWithContext(toolsUrl, fetchContext, { headers }), + ); + return response; + } catch (error) { + logger.error("Fetching tools", error); + return Promise.reject({ message: "Error fetching tools" }); + } +}; diff --git a/ui-next/src/components/flow/components/RichAddTaskMenu/state/types.ts b/ui-next/src/components/flow/components/RichAddTaskMenu/state/types.ts new file mode 100644 index 0000000000..7482f10da3 --- /dev/null +++ b/ui-next/src/components/flow/components/RichAddTaskMenu/state/types.ts @@ -0,0 +1,173 @@ +import { ReactElement } from "react"; +import { NodeData, PortData } from "reaflow"; +import { AuthHeaders, FormTaskType, IntegrationDef, WorkflowDef } from "types"; + +export type TaskDefinition = { + name: string; + description: string; +}; + +export type OperationContextData = { + id: string; + port: PortData; + node: NodeData; +}; + +export type BaseTaskMenuItem = { + category: string; + name: string; + description: string; + type: FormTaskType; + version?: number; + flagHidden?: boolean; +}; + +export type IntegrationMenuItem = BaseTaskMenuItem & { + integrationType: string; + iconName: string; + status?: string; +}; + +export type TaskMenuItem = BaseTaskMenuItem & { + name: string; + description: string; + onClick: () => void; + icon: ReactElement; + category?: string; + version?: number; +}; + +export enum RichAddMenuTabs { + ALL_TAB = "ALL", + SYSTEMS_TAB = "System", + OPERATORS_TAB = "Operators", + ALERTING_TAB = "Alerting", + WORKERS_TAB = "Workers", + AI_AGENTS_TAB = "AI Tasks", + SUB_WORKFLOWS_TAB = "Sub Workflows", + INTEGRATIONS_TAB = "Integrations", +} + +export type IntegrationDrillDownMenuProp = { + isOpen: boolean; + selectedIntegration: IntegrationMenuItem | null; + selectedRootIntegration: IntegrationMenuItem | null; + level: "integrations" | "tools"; + selectedIntegrationTools?: Record[]; +}; + +export interface RichAddTaskMenuMachineContext { + taskDefinitions: TaskDefinition[]; + workflowDefinitions: WorkflowDef[]; + workerMenuItems: BaseTaskMenuItem[]; + workflowMenuItems: BaseTaskMenuItem[]; + operationContext?: OperationContextData; + authHeaders?: AuthHeaders; + searchQuery: string; + nodes: NodeData[]; + hoveredItem: string; + selectedTab: RichAddMenuTabs; + isTaskDefFetched: boolean; + isSubWfFetched: boolean; + isIntegrationsFetched: boolean; + lastScrollTopPosition: number; + toScrollTop: number; + baseTaskMenuItems: BaseTaskMenuItem[]; + menuType: "quick" | "advanced"; + supportedIntegrations: IntegrationMenuItem[]; + availableIntegrations: IntegrationMenuItem[]; + integrationDefs?: IntegrationDef[]; + integrationDrillDownMenu: IntegrationDrillDownMenuProp; +} + +export enum MainStates { + INIT = "init", + CLOSED = "closed", + IDLE = "idle", + FETCH_FOR_TASK_DEFINITIONS = "fetchForTaskDefinitions", + FETCH_FOR_WORKFLOW_DEFINITIONS = "fetchForWorkflowDefinitions", + FETCH_FOR_INTEGRATIONS = "fetchForIntegrations", + WITH_TASK_DEFINITIONS = "withTaskDefinitions", + WITH_WORKFLOW_DEFINITIONS = "withWorkflowDefinitions", + WITH_INTEGRATIONS = "withIntegrations", + SEARCH_FIELD = "searchField", + FETCH_FOR_INTEGRATION_TOOLS = "fetchForIntegrationTools", +} + +export enum RichAddTaskMenuEventTypes { + TYPING = "TYPING", + CLOSE_MENU = "CLOSE_MENU", + GOT_TO_END = "GOT_TO_END", + SET_HOVERED_ITEM = "SET_HOVERED_ITEM", + SET_SELECTED_TAB = "SET_SELECTED_TAB", + SCROLL_TO_TOP = "SCROLL_TO_TOP", + SET_MENU_TYPE = "SET_MENU_TYPE", + FETCH_INTEGRATION_TOOLS = "FETCH_INTEGRATION_TOOLS", + SET_SELECTED_INTEGRATION = "SET_SELECTED_INTEGRATION", + REFETCH_INTEGRATIONS = "REFETCH_INTEGRATIONS", + UPDATE_INTEGRATION_DRILL_DOWN = "UPDATE_INTEGRATION_DRILL_DOWN", + SWITCH_TO_INTEGRATIONS = "SWITCH_TO_INTEGRATIONS", +} + +export type TypingEvent = { + type: RichAddTaskMenuEventTypes.TYPING; + text: string; +}; + +export type CloseMenuEvent = { + type: RichAddTaskMenuEventTypes.CLOSE_MENU; +}; + +export type GotToEndEvent = { + type: RichAddTaskMenuEventTypes.GOT_TO_END; + lastScrollTopPosition: number; +}; +export type ScrollToTopEvent = { + type: RichAddTaskMenuEventTypes.SCROLL_TO_TOP; +}; + +export type SetHoveredItemEvent = { + type: RichAddTaskMenuEventTypes.SET_HOVERED_ITEM; + data: string; +}; + +export type SetSelectedTabEvent = { + type: RichAddTaskMenuEventTypes.SET_SELECTED_TAB; + tab: RichAddMenuTabs; +}; + +export type SetMenuTypeEvent = { + type: RichAddTaskMenuEventTypes.SET_MENU_TYPE; + menuType: "quick" | "advanced"; +}; + +export type FetchIntegrationToolsEvent = { + type: RichAddTaskMenuEventTypes.FETCH_INTEGRATION_TOOLS; + integration: IntegrationMenuItem; +}; + +export type RefetchIntegrationsEvent = { + type: RichAddTaskMenuEventTypes.REFETCH_INTEGRATIONS; +}; + +export type UpdateIntegrationDrillDownEvent = { + type: RichAddTaskMenuEventTypes.UPDATE_INTEGRATION_DRILL_DOWN; + data: IntegrationDrillDownMenuProp; +}; + +export type SwitchToIntegrationsEvent = { + type: RichAddTaskMenuEventTypes.SWITCH_TO_INTEGRATIONS; +}; + +export type RichAddTaskMenuEvents = + | TypingEvent + | CloseMenuEvent + | GotToEndEvent + | SetHoveredItemEvent + | SetSelectedTabEvent + | ScrollToTopEvent + | SetMenuTypeEvent + | FetchIntegrationToolsEvent + | RefetchIntegrationsEvent + | UpdateIntegrationDrillDownEvent + | SwitchToIntegrationsEvent; diff --git a/ui-next/src/components/flow/components/RichAddTaskMenu/supportedTasks.ts b/ui-next/src/components/flow/components/RichAddTaskMenu/supportedTasks.ts new file mode 100644 index 0000000000..caf12b4127 --- /dev/null +++ b/ui-next/src/components/flow/components/RichAddTaskMenu/supportedTasks.ts @@ -0,0 +1,269 @@ +/** + * Supported Tasks Configuration + * + * This module defines the task types available in the "Add Task" menu. + * Core OSS tasks are defined here, while enterprise tasks are registered + * via the plugin system. + */ + +import { pluginRegistry } from "plugins/registry"; +import { TaskType } from "types"; +import { BaseTaskMenuItem, RichAddMenuTabs } from "./state/types"; + +/** + * Core OSS System Tasks + * These are fundamental system tasks available in open source Conductor. + */ +export const SYSTEM_TASKS: BaseTaskMenuItem[] = [ + { + name: "Event Task", + description: + "Publish an event to a messaging system (Kafka, AMQP, SQS, NATS, MQ).", + type: TaskType.EVENT, + category: "System", + }, + { + name: "HTTP Task", + type: TaskType.HTTP, + description: "Call an API / Microservice.", + category: "System", + }, + { + name: "HTTP Poll Task", + description: + "Poll a remote endpoint periodically until a condition is met. Useful for long running jobs.", + type: TaskType.HTTP_POLL, + category: "System", + }, + { + name: "gRPC Task", + description: "Call a gRPC service method.", + type: TaskType.GRPC, + category: "System", + }, + { + name: "Inline Task", + description: + "Run lightweight javascript code. Useful for data transformation.", + type: TaskType.INLINE, + category: "System", + }, + { + name: "JSON JQ Transform", + description: "Use the power of JQ to transform JSON.", + type: TaskType.JSON_JQ_TRANSFORM, + category: "System", + }, + { + name: "Business Rule Task", + description: "Evaluate business rules using Drools.", + type: TaskType.BUSINESS_RULE, + category: "System", + }, + { + name: "SQL Query", + description: "Run SQL query against a database.", + type: TaskType.JDBC, + category: "System", + }, + { + name: "Get Signed JWT Task", + description: "Get signed JWT task.", + type: TaskType.GET_SIGNED_JWT, + category: "System", + }, + { + name: "Update Task", + description: "Update existing task with new status and properties.", + type: TaskType.UPDATE_TASK, + category: "System", + }, + { + name: "Query Processor", + description: "Query from different data sources.", + type: TaskType.QUERY_PROCESSOR, + category: "System", + }, +]; + +/** + * Core OSS Operator Tasks + * These are control flow operators available in open source Conductor. + */ +export const OPERATOR_TASKS: BaseTaskMenuItem[] = [ + { + name: "Switch", + description: "if..then...else.", + type: TaskType.SWITCH, + category: RichAddMenuTabs.OPERATORS_TAB, + }, + { + name: "Do While", + description: "Loop.", + type: TaskType.DO_WHILE, + category: RichAddMenuTabs.OPERATORS_TAB, + }, + { + name: "Wait", + description: + "Add timer in your workflow. Wait for specific duration, time or a signal.", + type: TaskType.WAIT, + category: RichAddMenuTabs.OPERATORS_TAB, + }, + { + name: "Dynamic Task", + description: "Execute a task dynamically.", + type: TaskType.DYNAMIC, + category: RichAddMenuTabs.OPERATORS_TAB, + }, + { + name: "Set Variable", + description: "Set a variable.", + type: TaskType.SET_VARIABLE, + category: RichAddMenuTabs.OPERATORS_TAB, + }, + { + name: "Sub Workflow", + description: "Execute a sub workflow.", + type: TaskType.SUB_WORKFLOW, + category: RichAddMenuTabs.OPERATORS_TAB, + }, + { + name: "Terminate Workflow", + description: "Terminate another workflow.", + type: TaskType.TERMINATE_WORKFLOW, + category: RichAddMenuTabs.OPERATORS_TAB, + }, + { + name: "Terminate", + description: "Terminate the workflow.", + type: TaskType.TERMINATE, + category: RichAddMenuTabs.OPERATORS_TAB, + }, + { + name: "Fork Join", + description: "Run multiple tasks in parallel.", + type: TaskType.FORK_JOIN, + category: RichAddMenuTabs.OPERATORS_TAB, + }, + { + name: "Dynamic Fork", + description: "Spawn multiple tasks dynamically.", + type: TaskType.FORK_JOIN_DYNAMIC, + category: RichAddMenuTabs.OPERATORS_TAB, + }, + { + name: "Start Workflow Task", + description: "Start Workflow starts another workflow.", + type: TaskType.START_WORKFLOW, + category: RichAddMenuTabs.OPERATORS_TAB, + }, + { + name: "Get Workflow", + description: "Get workflow details", + type: TaskType.GET_WORKFLOW, + category: RichAddMenuTabs.OPERATORS_TAB, + }, + { + name: "Yield", + description: "Yield task", + type: TaskType.YIELD, + category: RichAddMenuTabs.OPERATORS_TAB, + }, +]; + +/** + * Core OSS Worker Tasks + */ +export const WORKER_TASKS: BaseTaskMenuItem[] = [ + { + name: "Worker Task (Simple)", + description: "Runs a Worker task.", + type: TaskType.SIMPLE, + category: RichAddMenuTabs.WORKERS_TAB, + }, +]; + +/** + * Get all plugin-registered task menu items + */ +const getPluginTaskMenuItems = (): BaseTaskMenuItem[] => { + const pluginItems = pluginRegistry.getTaskMenuItems(); + // Convert plugin items to BaseTaskMenuItem format + return pluginItems.map((item) => ({ + name: item.name, + description: item.description, + type: item.type as any, // FormTaskType + category: item.category, + version: item.version, + flagHidden: item.hidden, + })); +}; + +/** + * AI/LLM Tasks for Agentic Orchestration + * These are AI-powered tasks for building intelligent workflows. + */ +export const AI_TASKS: BaseTaskMenuItem[] = [ + { + name: "LLM Chat Complete", + description: "Generate text using a large language model chat interface.", + type: TaskType.LLM_CHAT_COMPLETE, + category: RichAddMenuTabs.AI_AGENTS_TAB, + }, + { + name: "LLM Text Complete", + description: "Generate text using a large language model completion API.", + type: TaskType.LLM_TEXT_COMPLETE, + category: RichAddMenuTabs.AI_AGENTS_TAB, + }, + { + name: "LLM Generate Embeddings", + description: "Generate vector embeddings from text.", + type: TaskType.LLM_GENERATE_EMBEDDINGS, + category: RichAddMenuTabs.AI_AGENTS_TAB, + }, + { + name: "LLM Get Embeddings", + description: "Retrieve stored embeddings by ID or query.", + type: TaskType.LLM_GET_EMBEDDINGS, + category: RichAddMenuTabs.AI_AGENTS_TAB, + }, + { + name: "LLM Index Document", + description: "Index a document into a vector database for semantic search.", + type: TaskType.LLM_INDEX_DOCUMENT, + category: RichAddMenuTabs.AI_AGENTS_TAB, + }, + { + name: "LLM Search Index", + description: "Search indexed documents using semantic similarity.", + type: TaskType.LLM_SEARCH_INDEX, + category: RichAddMenuTabs.AI_AGENTS_TAB, + }, +]; + +/** + * @deprecated Use AI_TASKS instead + */ +export const LLM_TASKS: BaseTaskMenuItem[] = AI_TASKS; + +const [simpleTask, ...remainingWorkerTasks] = WORKER_TASKS; + +/** + * Returns all available tasks including plugin-registered tasks. + * Called at runtime so plugin items (e.g. Wait For Webhook Task) are included when the menu opens. + */ +export const getALL_TASKS = (): BaseTaskMenuItem[] => [ + simpleTask, + ...SYSTEM_TASKS, + ...OPERATOR_TASKS, + ...AI_TASKS, + ...getPluginTaskMenuItems(), + ...remainingWorkerTasks, +]; + +/** + * @deprecated Use getALL_TASKS() so plugin items are included (ALL_TASKS is computed at module load and may miss plugins). + */ +export const ALL_TASKS: BaseTaskMenuItem[] = getALL_TASKS(); diff --git a/ui-next/src/components/flow/components/RichAddTaskMenu/taskGenerator.ts b/ui-next/src/components/flow/components/RichAddTaskMenu/taskGenerator.ts new file mode 100644 index 0000000000..3789559fcb --- /dev/null +++ b/ui-next/src/components/flow/components/RichAddTaskMenu/taskGenerator.ts @@ -0,0 +1,783 @@ +import { randomChars as dynamicTaskSuffixGenerator } from "utils"; +import { + BusinessRuleTaskDef, + DoWhileTaskDef, + DynamicTaskDef, + EventTaskDef, + ForkJoinDynamicDef, + ForkJoinTaskDef, + HTTPMethods, + HttpPollTaskDef, + HttpTaskDef, + HumanTaskDef, + InlineTaskDef, + JDBCTaskDef, + JDBCType, + JoinTaskDef, + JsonJQTransformTaskDef, + KafkaPublishTaskDef, + PollingStrategy, + SendgridTaskDef, + SetVariableTaskDef, + SimpleTaskDef, + StartWorkflowTaskDef, + SubWorkflowTaskDef, + SwitchTaskDef, + TaskType, + TerminateTaskDef, + TerminateWorkflowTaskDef, + WaitForWebHookTaskDef, + WaitTaskDef, + UpdateSecretTaskDef, + LLMTaskTypes, + LLMTextCompleteTaskDef, + LLMGenerateEmbeddings, + LLMGetEmbeddings, + LLMStoreEmbeddings, + LLMIndexDocument, + LLMSearchIndex, + LLMIndexText, + FormTaskType, + GetDocumentTaskDef, + QueryProcessorTaskDef, + QueryProcessorType, + OpsGenieTaskDef, + GetSignedJWTTaskDef, + GetSignedJWTAlgorithmType, + AssignmentCompletionStrategy, + UpdateTaskDef, + GetWorkflowDef, + LLMChatComplete, + GrpcTaskDef, + YieldTaskDef, + MCPTaskDef, + ChunkTextTaskDef, + ListFilesTaskDef, + ParseDocumentTaskDef, +} from "types"; +import { HTTP_TEST_ENDPOINT } from "utils/constants/common"; +import { BaseTaskMenuItem } from "./state/types"; +import { UpdateTaskStatus } from "types/UpdateTaskStatus"; + +// THIS FILE SHOULD COME FROM THE SDK + +const generateNameAndTaskReference = ( + aParam: string, + suffixGenerator = dynamicTaskSuffixGenerator, +) => { + const suffix = suffixGenerator(); + return { + name: `${aParam}_task_${suffix}`, + taskReferenceName: `${aParam}_task_${suffix}_ref`, + }; +}; + +export type NameGeneratorFn = typeof generateNameAndTaskReference; + +export interface GenerateTaskNoJoinParams { + overrides?: Partial; + nameGenerator?: NameGeneratorFn; +} + +type SomeFork = ForkJoinTaskDef | ForkJoinDynamicDef; + +export interface GenerateTaskJoinParams extends GenerateTaskNoJoinParams { + joinOverrides?: Partial; +} + +export type GenerateTaskFn = T extends SomeFork + ? (params: GenerateTaskJoinParams) => [T, JoinTaskDef] + : (params: GenerateTaskNoJoinParams) => T; + +const DEFAULT_ARGS = { + overrides: {}, + joinOverrides: {}, + nameGenerator: generateNameAndTaskReference, +}; + +export const generateEventTask: GenerateTaskFn = ({ + overrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): EventTaskDef => { + return { + ...nameGenerator("event"), + type: TaskType.EVENT, + sink: "sqs:internal_event_name", + inputParameters: {}, + ...overrides, + }; +}; + +export const generateSimpleTask: GenerateTaskFn = ({ + overrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): SimpleTaskDef => { + return { + ...nameGenerator("simple"), + type: TaskType.SIMPLE, + ...overrides, + }; +}; + +export const generateYieldTask: GenerateTaskFn = ({ + overrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): YieldTaskDef => { + return { + ...nameGenerator("yield"), + type: TaskType.YIELD, + ...overrides, + }; +}; + +export const generateHTTPTask: GenerateTaskFn = ({ + overrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): HttpTaskDef => ({ + ...nameGenerator("http"), + type: TaskType.HTTP, + inputParameters: { + uri: HTTP_TEST_ENDPOINT, + method: HTTPMethods.GET, + accept: "application/json", + contentType: "application/json", + encode: true, + }, + ...overrides, +}); + +export const generateGRPCTask: GenerateTaskFn = ({ + overrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): GrpcTaskDef => ({ + ...nameGenerator("grpc"), + type: TaskType.GRPC, + ...overrides, +}); + +export const generateMCPTask: GenerateTaskFn = ({ + overrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): MCPTaskDef => ({ + ...nameGenerator("integration"), + type: TaskType.MCP, + ...overrides, +}); + +export const generateHTTPPollTask: GenerateTaskFn = ({ + overrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): HttpPollTaskDef => ({ + ...nameGenerator("http_poll"), + type: TaskType.HTTP_POLL, + inputParameters: { + http_request: { + uri: HTTP_TEST_ENDPOINT, + method: HTTPMethods.GET, + accept: "application/json", + contentType: "application/json", + terminationCondition: + "(function(){ return $.output.response.body.randomInt > 10;})();", + pollingInterval: "60", + pollingStrategy: PollingStrategy.FIXED, + encode: true, + }, + }, + ...overrides, +}); + +export const generateJSONJQTransform: GenerateTaskFn< + JsonJQTransformTaskDef +> = ({ + overrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): JsonJQTransformTaskDef => ({ + ...nameGenerator("json_transform"), + type: TaskType.JSON_JQ_TRANSFORM, + inputParameters: { + persons: [ + { + name: "some", + last: "name", + email: "mail@mail.com", + id: 1, + }, + { + name: "some2", + last: "name2", + email: "mail2@mail.com", + id: 2, + }, + ], + queryExpression: ".persons | map({user:{email,id}})", + }, + ...overrides, +}); + +export const generateInlineTask: GenerateTaskFn = ({ + overrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): InlineTaskDef => ({ + ...nameGenerator("inline"), + type: TaskType.INLINE, + inputParameters: { + expression: "(function () {\n return $.value1 + $.value2;\n})();", + evaluatorType: "graaljs", + value1: 1, + value2: 2, + }, + ...overrides, +}); + +export const generateKafkaPublishTask: GenerateTaskFn = ({ + overrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): KafkaPublishTaskDef => ({ + ...nameGenerator("kafka_publish"), + type: TaskType.KAFKA_PUBLISH, + inputParameters: { + kafka_request: { + topic: "userTopic", + value: "Message to publish", + bootStrapServers: "localhost:9092", + headers: { + "X-Auth": "Auth-key", + }, + key: "valuekey", + keySerializer: "org.apache.kafka.common.serialization.IntegerSerializer", + }, + }, + ...overrides, +}); + +export const generateJoinTask: GenerateTaskFn = ({ + overrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): JoinTaskDef => ({ + ...nameGenerator("join"), + inputParameters: {}, + type: TaskType.JOIN, + joinOn: [], + optional: false, + asyncComplete: false, + ...overrides, +}); + +export const generateForkJoinTasks: GenerateTaskFn = ({ + overrides: forkOverrides = {}, + joinOverrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): [ForkJoinTaskDef, JoinTaskDef] => [ + { + ...nameGenerator("fork"), + inputParameters: {}, + type: TaskType.FORK_JOIN, + defaultCase: [], + forkTasks: [[]], // TODO check this in the mapper. array of array else it will break + ...forkOverrides, + } as ForkJoinTaskDef, + generateJoinTask({ overrides: joinOverrides, nameGenerator }), +]; + +export const generateSwitchTasks: GenerateTaskFn = ({ + overrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): SwitchTaskDef => ({ + ...nameGenerator("switch"), + inputParameters: { + switchCaseValue: "", + }, + type: TaskType.SWITCH, + decisionCases: {}, + defaultCase: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + ...overrides, +}); + +export const generateDoWhileTask: GenerateTaskFn = ({ + overrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): DoWhileTaskDef => ({ + ...nameGenerator("do_while"), + inputParameters: {}, + type: TaskType.DO_WHILE, + startDelay: 0, + optional: false, + asyncComplete: false, + loopCondition: "", + evaluatorType: "value-param", + loopOver: [], + ...overrides, +}); + +export const generateDynamicForkTasks: GenerateTaskFn = ({ + overrides: dynamicOverrides = {}, + joinOverrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): [ForkJoinDynamicDef, JoinTaskDef] => [ + { + ...nameGenerator("fork_join_dynamic"), + inputParameters: { + dynamicTasks: "", + dynamicTasksInput: "", + }, + type: TaskType.FORK_JOIN_DYNAMIC, + dynamicForkTasksParam: "dynamicTasks", + dynamicForkTasksInputParamName: "dynamicTasksInput", + startDelay: 0, + optional: false, + asyncComplete: false, + ...dynamicOverrides, + } as ForkJoinDynamicDef, + { + ...nameGenerator("join"), + inputParameters: {}, + type: TaskType.JOIN, + joinOn: [], + optional: false, + asyncComplete: false, + ...joinOverrides, + }, +]; + +export const generateWaitTask: GenerateTaskFn = ({ + overrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): WaitTaskDef => ({ + ...nameGenerator("wait"), + type: TaskType.WAIT, + ...overrides, +}); + +export const generateDynamicTask: GenerateTaskFn = ({ + overrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): DynamicTaskDef => ({ + ...nameGenerator("dynamic"), + inputParameters: { + taskToExecute: "", + }, + type: TaskType.DYNAMIC, + dynamicTaskNameParam: "taskToExecute", + ...overrides, +}); + +export const generateTerminateTask: GenerateTaskFn = ({ + overrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): TerminateTaskDef => ({ + ...nameGenerator("terminate"), + inputParameters: { + terminationStatus: "COMPLETED", + }, + type: TaskType.TERMINATE, + startDelay: 0, + optional: false, + ...overrides, +}); + +export const generateSetVariableTask: GenerateTaskFn = ({ + overrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): SetVariableTaskDef => ({ + ...nameGenerator("set_variable"), + type: TaskType.SET_VARIABLE, + inputParameters: { + name: "Orkes", + }, + ...overrides, +}); + +export const generateSubWorkflowTask: GenerateTaskFn = ({ + overrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): SubWorkflowTaskDef => ({ + ...nameGenerator("sub_workflow"), + inputParameters: {}, + type: TaskType.SUB_WORKFLOW, + subWorkflowParam: { + name: "", + }, + ...overrides, +}); + +export const generateTerminateWorkflowTask: GenerateTaskFn< + TerminateWorkflowTaskDef +> = ({ + overrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): TerminateWorkflowTaskDef => ({ + ...nameGenerator("TW"), + inputParameters: { + workflowId: [""], + terminationReason: "", + triggerFailureWorkflow: false, + }, + type: TaskType.TERMINATE_WORKFLOW, + ...overrides, +}); + +export const generateBusinessRuleTask: GenerateTaskFn = ({ + overrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): BusinessRuleTaskDef => ({ + ...nameGenerator("business_rule"), + inputParameters: { + ruleFileLocation: "https://business-rules.s3.amazonaws.com/rules.xlsx", + executionStrategy: "FIRE_FIRST", + cacheTimeoutMinutes: 60, + inputColumns: {}, + outputColumns: [], + }, + type: TaskType.BUSINESS_RULE, + ...overrides, +}); + +export const generateSendgridTask: GenerateTaskFn = ({ + overrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): SendgridTaskDef => ({ + ...nameGenerator("sendgrid"), + inputParameters: { + from: "", + to: "", + subject: "", + contentType: "text/plain", + content: "", + sendgridConfiguration: "", + }, + type: TaskType.SENDGRID, + ...overrides, +}); + +export const generateStartWorkflowTask: GenerateTaskFn< + StartWorkflowTaskDef +> = ({ + overrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): StartWorkflowTaskDef => ({ + ...nameGenerator("start_workflow"), + inputParameters: { + startWorkflow: { + name: "", + input: {}, + }, + }, + type: TaskType.START_WORKFLOW, + ...overrides, +}); + +export const generateWaitForWebhookTask: GenerateTaskFn< + WaitForWebHookTaskDef +> = ({ + overrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): WaitForWebHookTaskDef => ({ + ...nameGenerator("webhook"), + inputParameters: { + matches: { + "$['event']['type']": "message", + "$['event']['text']": "Hello", + }, + }, + type: TaskType.WAIT_FOR_WEBHOOK, + ...overrides, +}); + +export const generateHumanTask: GenerateTaskFn = ({ + overrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): HumanTaskDef => ({ + ...nameGenerator("human"), + inputParameters: { + __humanTaskDefinition: { + assignmentCompletionStrategy: AssignmentCompletionStrategy.LEAVE_OPEN, + assignments: [], + }, + }, + type: TaskType.HUMAN, + ...overrides, +}); + +export const generateJDBCTask: GenerateTaskFn = ({ + overrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): JDBCTaskDef => { + return { + ...nameGenerator("jdbc"), + inputParameters: { + integrationName: "", + statement: "SELECT * FROM tableName WHERE id=?", + parameters: [], + type: JDBCType.SELECT, + }, + type: TaskType.JDBC, + ...overrides, + }; +}; + +export const generateChunkTextTask: GenerateTaskFn = ({ + overrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): ChunkTextTaskDef => ({ + ...nameGenerator("chunk_text"), + inputParameters: { + text: "", + chunkSize: 1024, + mediaType: "auto", + }, + type: TaskType.CHUNK_TEXT, + ...overrides, +}); + +export const generateParseDocumentTask: GenerateTaskFn< + ParseDocumentTaskDef +> = ({ + overrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): ParseDocumentTaskDef => { + return { + ...nameGenerator("parse_document"), + inputParameters: { + integrationName: "", + url: "", + mediaType: "auto", + chunkSize: 0, + }, + type: TaskType.PARSE_DOCUMENT, + ...overrides, + }; +}; + +type AILLMTaskTypes = + | TaskType.LLM_TEXT_COMPLETE + | TaskType.LLM_GENERATE_EMBEDDINGS + | TaskType.LLM_GET_EMBEDDINGS + | TaskType.LLM_STORE_EMBEDDINGS + | TaskType.LLM_INDEX_DOCUMENT + | TaskType.LLM_SEARCH_INDEX + | TaskType.GET_DOCUMENT + | TaskType.LLM_INDEX_TEXT + | TaskType.LLM_CHAT_COMPLETE; + +export const generateAITask = ( + type: AILLMTaskTypes, +) => { + const taskGen = ({ + overrides = {}, + nameGenerator = generateNameAndTaskReference, + }): T => { + const taskProps = { + ...nameGenerator(type.toLowerCase()), + inputParameters: {}, + type: type, + ...overrides, + }; + const typedProps = { + [TaskType.LLM_TEXT_COMPLETE]: taskProps as LLMTextCompleteTaskDef, + [TaskType.LLM_GENERATE_EMBEDDINGS]: taskProps as LLMGenerateEmbeddings, + [TaskType.LLM_GET_EMBEDDINGS]: taskProps as LLMGetEmbeddings, + [TaskType.LLM_STORE_EMBEDDINGS]: taskProps as LLMStoreEmbeddings, + [TaskType.LLM_INDEX_DOCUMENT]: taskProps as LLMIndexDocument, + [TaskType.LLM_SEARCH_INDEX]: taskProps as LLMSearchIndex, + [TaskType.LLM_INDEX_TEXT]: taskProps as LLMIndexText, + [TaskType.GET_DOCUMENT]: taskProps as GetDocumentTaskDef, + [TaskType.LLM_CHAT_COMPLETE]: taskProps as LLMChatComplete, + } satisfies Record; + + return typedProps[type] as T; + }; + return taskGen as GenerateTaskFn; +}; + +export const generateUpdateSecretTask: GenerateTaskFn = ({ + overrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): UpdateSecretTaskDef => { + return { + ...nameGenerator("update_secret"), + inputParameters: { + _secrets: { + secretKey: "my_token", + secretValue: "input secret value here", + }, + }, + type: TaskType.UPDATE_SECRET, + ...overrides, + }; +}; + +export const generateGetSignedJWTTask: GenerateTaskFn = ({ + overrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): GetSignedJWTTaskDef => { + return { + ...nameGenerator("get_signed_jwt"), + inputParameters: { + subject: "", + issuer: "", + privateKey: "", + privateKeyId: "", + audience: "", + ttlInSecond: 0, + scopes: [], + algorithm: GetSignedJWTAlgorithmType.RS256, + }, + type: TaskType.GET_SIGNED_JWT, + ...overrides, + }; +}; + +export const generateQueryProcessorTask: GenerateTaskFn< + QueryProcessorTaskDef +> = ({ + overrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): QueryProcessorTaskDef => { + return { + ...nameGenerator("query_processor"), + inputParameters: { + workflowNames: [], + statuses: [], + correlationIds: [], + queryType: QueryProcessorType.CONDUCTOR_API, + }, + type: TaskType.QUERY_PROCESSOR, + ...overrides, + }; +}; + +export const generateOpsGenieTask: GenerateTaskFn = ({ + overrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): OpsGenieTaskDef => { + return { + ...nameGenerator("Opsgenie"), + inputParameters: { + alias: "", + description: "", + visibleTo: [ + { + id: "id-1", + type: "type-1", + }, + { + id: "id-2", + type: "type-2", + }, + ], + message: "", + responders: [ + { + type: "user", + username: "someone@someone.com", + }, + ], + details: {}, + }, + ...overrides, + type: TaskType.OPS_GENIE, + }; +}; + +export const generateUpdateTask: GenerateTaskFn = ({ + overrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): UpdateTaskDef => { + return { + ...nameGenerator("update_task"), + inputParameters: { + taskStatus: UpdateTaskStatus.COMPLETED, + mergeOutput: false, + workflowId: "${workflow.workflowId}", + taskRefName: "", + }, + ...overrides, + type: TaskType.UPDATE_TASK, + }; +}; + +export const generateGetWorkflowTask: GenerateTaskFn = ({ + overrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): GetWorkflowDef => { + return { + ...nameGenerator("get_workflow"), + inputParameters: { + id: "", + includeTasks: false, + }, + ...overrides, + type: TaskType.GET_WORKFLOW, + }; +}; + +export const generateListFilesTask: GenerateTaskFn = ({ + overrides = {}, + nameGenerator = generateNameAndTaskReference, +} = DEFAULT_ARGS): ListFilesTaskDef => ({ + ...nameGenerator("list_files"), + type: TaskType.LIST_FILES, + inputParameters: { + inputLocation: "", + fileTypes: [], + }, + ...overrides, +}); + +export const taskGeneratorMap = { + [TaskType.WAIT]: generateWaitTask, + [TaskType.HTTP]: generateHTTPTask, + [TaskType.KAFKA_PUBLISH]: generateKafkaPublishTask, + [TaskType.HUMAN]: generateHumanTask, + [TaskType.BUSINESS_RULE]: generateBusinessRuleTask, + [TaskType.SENDGRID]: generateSendgridTask, + [TaskType.WAIT_FOR_WEBHOOK]: generateWaitForWebhookTask, + [TaskType.HTTP_POLL]: generateHTTPPollTask, + [TaskType.DO_WHILE]: generateDoWhileTask, + [TaskType.SIMPLE]: generateSimpleTask, + [TaskType.YIELD]: generateYieldTask, + [TaskType.JDBC]: generateJDBCTask, + [TaskType.EVENT]: generateEventTask, + [TaskType.JOIN]: generateJoinTask, + [TaskType.FORK_JOIN]: generateForkJoinTasks, + [TaskType.FORK_JOIN_DYNAMIC]: generateDynamicForkTasks, + [TaskType.DYNAMIC]: generateDynamicTask, + [TaskType.INLINE]: generateInlineTask, + [TaskType.SWITCH]: generateSwitchTasks, + [TaskType.JSON_JQ_TRANSFORM]: generateJSONJQTransform, + [TaskType.TERMINATE]: generateTerminateTask, + [TaskType.SET_VARIABLE]: generateSetVariableTask, + [TaskType.TERMINATE_WORKFLOW]: generateTerminateWorkflowTask, + [TaskType.SUB_WORKFLOW]: generateSubWorkflowTask, + [TaskType.START_WORKFLOW]: generateStartWorkflowTask, + [TaskType.LLM_TEXT_COMPLETE]: generateAITask(TaskType.LLM_TEXT_COMPLETE), + [TaskType.LLM_GENERATE_EMBEDDINGS]: generateAITask( + TaskType.LLM_GENERATE_EMBEDDINGS, + ), + [TaskType.LLM_GET_EMBEDDINGS]: generateAITask(TaskType.LLM_GET_EMBEDDINGS), + [TaskType.LLM_STORE_EMBEDDINGS]: generateAITask( + TaskType.LLM_STORE_EMBEDDINGS, + ), + [TaskType.LLM_INDEX_DOCUMENT]: generateAITask(TaskType.LLM_INDEX_DOCUMENT), + [TaskType.LLM_SEARCH_INDEX]: generateAITask(TaskType.LLM_SEARCH_INDEX), + [TaskType.LLM_INDEX_TEXT]: generateAITask(TaskType.LLM_INDEX_TEXT), + [TaskType.UPDATE_SECRET]: generateUpdateSecretTask, + [TaskType.GET_DOCUMENT]: generateAITask(TaskType.GET_DOCUMENT), + [TaskType.QUERY_PROCESSOR]: generateQueryProcessorTask, + [TaskType.OPS_GENIE]: generateOpsGenieTask, + [TaskType.GET_SIGNED_JWT]: generateGetSignedJWTTask, + [TaskType.UPDATE_TASK]: generateUpdateTask, + [TaskType.GET_WORKFLOW]: generateGetWorkflowTask, + [TaskType.LLM_CHAT_COMPLETE]: generateAITask(TaskType.LLM_CHAT_COMPLETE), + [TaskType.GRPC]: generateGRPCTask, + [TaskType.MCP]: generateMCPTask, + [TaskType.CHUNK_TEXT]: generateChunkTextTask, + [TaskType.LIST_FILES]: generateListFilesTask, + [TaskType.PARSE_DOCUMENT]: generateParseDocumentTask, +} satisfies Record>; + +export const uniqueTaskIdGenerator = (sr: BaseTaskMenuItem) => { + return `${sr.category}-${sr.name}${sr.version ? sr.version : ""}`; +}; diff --git a/ui-next/src/components/flow/components/graphs/CustomEdgeButton.tsx b/ui-next/src/components/flow/components/graphs/CustomEdgeButton.tsx new file mode 100644 index 0000000000..021097ec1a --- /dev/null +++ b/ui-next/src/components/flow/components/graphs/CustomEdgeButton.tsx @@ -0,0 +1,257 @@ +import { FunctionComponent, useMemo } from "react"; +import { BOTTOM_PORT_MARGIN } from "components/flow/nodes/mapper/layout"; +import PlusIcon from "../shapes/TaskCard/icons/PlusIcon"; +import MinusIcon from "../shapes/TaskCard/icons/MinusIcon"; +import { PortChildProps } from "reaflow"; +import { TaskDef, Crumb } from "types"; +import { keyframes, styled } from "@mui/system"; +import { isSafari } from "utils/utils"; +import { useDroppableNode } from "components/flow/dragDrop"; +import { DraggedNodeData } from "components/flow/state"; +import classnames from "classnames"; + +type DataType = { + task: TaskDef; + crumbs: Crumb[]; +}; +type CustomEdgeButtonProps = PortChildProps & { + size: number; + hidden: boolean; + variant: "ADD" | "DELETE" | "ADD_DELETE"; + onDeleteClick: (event: any) => void; + onClick: (event: any) => void; + onEnter: (event: any) => void; + onLeave: (event: any) => void; + data: DataType; + nodeId: string; + activeEdgeId?: string; +}; + +const changeColor = keyframes` +0% { + background-position: left top, right bottom, left bottom, right top; +} +100% { + background-color: rgba(159,220,170,0.5); + background-position: left 15px top, right 15px bottom , left bottom 15px , right top 15px; +} +`; + +const pulseAnimation = keyframes` + 0% { + box-shadow: 0 0 8px 2px rgba(33, 150, 243, 0.5); + transform: scale(1); + } + 50% { + box-shadow: 0 0 12px 4px rgba(33, 150, 243, 0.7); + transform: scale(1.02); + } + 100% { + box-shadow: 0 0 8px 2px rgba(33, 150, 243, 0.5); + transform: scale(1); + } +`; + +const ActiveButtonStyle = styled("div")` + &.active { + animation: ${pulseAnimation} 1.5s ease-in-out infinite; + background-color: #e3f2fd; + border: 2px solid #2196f3; + } +`; + +const DroppablePlace = styled("div")<{ + dropIsDisabled: boolean; + draggedNodeData?: DraggedNodeData; +}>` + &.over { + background-image: + linear-gradient(90deg, silver 50%, transparent 50%), + linear-gradient(90deg, silver 50%, transparent 50%), + linear-gradient(0deg, silver 50%, transparent 50%), + linear-gradient(0deg, silver 50%, transparent 50%); + background-repeat: repeat-x, repeat-x, repeat-y, repeat-y; + background-size: + 15px 2px, + 15px 2px, + 2px 15px, + 2px 15px; + background-position: + left top, + right bottom, + left bottom, + right top; + animation: ${changeColor} 1s infinite linear; + } + + &.dragging { + } + position: absolute; + top: 10px; + height: ${(props) => + props.dropIsDisabled || props.draggedNodeData == null ? 0 : 80}px; + width: ${(props) => + props.dropIsDisabled || props.draggedNodeData == null + ? 0 + : props.draggedNodeData.width}px; +`; + +export const CustomEdgeButton: FunctionComponent = ({ + activeEdgeId, + x, + y, + size = 20, + hidden = true, + variant = "ADD", + onEnter = () => undefined, + onLeave = () => undefined, + onClick = () => undefined, + onDeleteClick = () => undefined, + data, + nodeId, + port, +}) => { + const { + droppableResult: { isOver, setNodeRef }, + draggedNodeData, + dropIsDisabled, + } = useDroppableNode({ + nodeData: data, + position: port.side === "NORTH" ? "ABOVE" : "BELOW", + nodeId, + }); + + const { translateX, translateY, offset } = useMemo(() => { + const half = size / 2; + const translateX = x - half; + const translateY = + y - (half + (port.side === "SOUTH" ? BOTTOM_PORT_MARGIN : 0)); + + const offset = isSafari ? 15 : 0; + + return { translateX, translateY, offset }; + }, [port.side, size, x, y]); + + return hidden ? null : ( + <> + + { + event.preventDefault(); + event.stopPropagation(); + onClick(event); + }} + width={size + 20} + height={size + 20} + > + {variant === "ADD" || variant === "DELETE" ? ( + { + event.preventDefault(); + event.stopPropagation(); + onClick(event); + }} + onMouseEnter={onEnter} + onMouseLeave={onLeave} + > + + {variant === "ADD" ? ( + + ) : ( + + )} + + ) : ( + +
    { + event.preventDefault(); + event.stopPropagation(); + onClick(event); + }} + > + +
    +
    { + event.preventDefault(); + event.stopPropagation(); + onDeleteClick(event); + }} + > + +
    +
    + )} +
    +
    + + ); +}; + +export default CustomEdgeButton; diff --git a/ui-next/src/components/flow/components/graphs/CustomLabel.tsx b/ui-next/src/components/flow/components/graphs/CustomLabel.tsx new file mode 100644 index 0000000000..464869a656 --- /dev/null +++ b/ui-next/src/components/flow/components/graphs/CustomLabel.tsx @@ -0,0 +1,85 @@ +import { FunctionComponent } from "react"; +import { getFlowTheme } from "components/flow/theme"; +import { EdgeData, LabelProps, NodeData, EdgeChildProps } from "reaflow"; +import { EDGE_SPACING } from "./PanAndZoomWrapper/constants"; + +const HORIZONTAL_PADDING = 10; + +type SelectEdgePram = { edge: EdgeData }; + +interface CustomLabelProps extends LabelProps { + selectEdge: (edgeData: SelectEdgePram) => void; + nodes: NodeData[]; + edgeChildProps: EdgeChildProps; +} + +export const CustomLabel: FunctionComponent> = ({ + text = "", + x = 0, + y = 0, + originalText = "", + edgeChildProps: edgeProps, + selectEdge = (_nonEdge) => {}, + ...labelProps +}) => { + const label = text; + const isDefault = edgeProps?.edge.data?.isDefault ?? false; + const theme = getFlowTheme(); + + // This should be `x + labelProps.width / 2`, + // but the width is already divided by 2 in Reaflow, see: + // https://github.com/reaviz/reaflow/blob/master/src/layout/elkLayout.ts#L262 + const labelPropsWidth = labelProps?.width || 0; + const centeredX = x + labelPropsWidth; + + const labelSize = { + // Multiplying width * 2 since it's already divided by 2 in Reaflow. + // HORIZONTAL_PADDING is added to both sides to make sure the label is not cut off. + width: labelPropsWidth * 2 + HORIZONTAL_PADDING * 2, + height: 30, + x: centeredX, + y: y, + }; + + const onClickLabel = () => { + if (edgeProps?.edge) selectEdge({ edge: edgeProps.edge }); + }; + + return ( + + +
    + {label} +
    +
    +
    + ); +}; diff --git a/ui-next/src/components/flow/components/graphs/CustomNode.jsx b/ui-next/src/components/flow/components/graphs/CustomNode.jsx new file mode 100644 index 0000000000..6c331fb8be --- /dev/null +++ b/ui-next/src/components/flow/components/graphs/CustomNode.jsx @@ -0,0 +1,94 @@ +import { Node } from "reaflow"; +import { TaskShape } from "../shapes/TaskShape"; +import CustomPort from "./CustomPort"; +import _first from "lodash/first"; + +import { isSafari } from "utils/utils"; + +export const CustomNode = (nodeProps) => { + const { + operationContext, + onClick, + onToggleTaskMenu, + onDeleteBranch, + properties: nodeProperties, + isInconsistent, + displayDescription = false, + } = nodeProps; + const portsHidden = _first(nodeProperties?.ports || [])?.hidden === true; + + return ( + null} + label={<>} + style={{ stroke: "none", fill: "none" }} + port={ + { + onToggleTaskMenu(e, { + id: port.id, + port, + node: nodeProperties, + }); + }} + onDeleteClick={(e, port) => { + onDeleteBranch(e, { + id: port.id, + port, + node: nodeProperties, + }); + }} + /> + } + > + {(event) => { + return ( + + { + onClick(e, nodeProperties); + }} + style={{ + overflow: "visible", + }} + height={event.height} + width={event.width} + > +
    +
    + +
    +
    +
    +
    + ); + }} +
    + ); +}; diff --git a/ui-next/src/components/flow/components/graphs/CustomPort.jsx b/ui-next/src/components/flow/components/graphs/CustomPort.jsx new file mode 100644 index 0000000000..74036fdbdb --- /dev/null +++ b/ui-next/src/components/flow/components/graphs/CustomPort.jsx @@ -0,0 +1,42 @@ +import { Port } from "reaflow"; +import CustomEdgeButton from "./CustomEdgeButton"; +import { TERMINAL_END_NAME } from "components/flow/nodes/mapper/constants"; + +const CustomPort = ({ + operationContext, + onClick, + onDeleteClick, + nodeProperties, + ...restProps +}) => { + const portVariant = + ["SWITCH", "FORK_JOIN"].includes(nodeProperties.data.task.type) && + restProps.properties.side === "SOUTH" + ? "ADD_DELETE" + : "ADD"; + + const isEndTerminal = nodeProperties.data.task.name === TERMINAL_END_NAME; + + return ( + + {(event) => { + return ( + !isEndTerminal && ( + + ); +}; + +export default CustomPort; diff --git a/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/PanAndZoomProvider.tsx b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/PanAndZoomProvider.tsx new file mode 100644 index 0000000000..fd911b6f3e --- /dev/null +++ b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/PanAndZoomProvider.tsx @@ -0,0 +1,19 @@ +import { ReactNode } from "react"; +import { ActorRef } from "xstate"; +import { PanAndZoomContext, PanAndZoomEvents } from "./state"; + +export interface PanAndZoomContextProps { + panAndZoomActor?: ActorRef; + children?: ReactNode; +} + +const PanAndZoomContextProvider = ({ + children, + panAndZoomActor, +}: PanAndZoomContextProps) => ( + + {children} + +); + +export default PanAndZoomContextProvider; diff --git a/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/PanAndZoomWrapper.tsx b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/PanAndZoomWrapper.tsx new file mode 100644 index 0000000000..73dd9edeac --- /dev/null +++ b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/PanAndZoomWrapper.tsx @@ -0,0 +1,314 @@ +import { Box } from "@mui/material"; +import { Handler } from "@use-gesture/core/types"; +import { useDrag, usePinch, useWheel } from "@use-gesture/react"; +import { useSelector } from "@xstate/react"; +import { FlowEvents } from "components/flow/state"; +import { selectWorkflowName } from "components/flow/state/selectors"; +import domToImage from "dom-to-image"; +import { + FunctionComponent, + ReactNode, + Ref, + useCallback, + useContext, + useEffect, + useRef, + WheelEvent, +} from "react"; +import { ColorModeContext } from "theme/material/ColorModeContext"; +import { ActorRef } from "xstate"; +import { MAX_ZOOM, MIN_ZOOM } from "./constants"; +import PanAndZoomContextProvider from "./PanAndZoomProvider"; +import { PanAndZoomEvents, usePanAndZoomActor } from "./state"; +import { ZoomControls } from "./ZoomControls"; + +const isEventReallyWheel = (event: WheelEvent) => { + return Math.abs(event.deltaY) > 25; +}; + +const printScreen = (workflowName: string) => { + const node = document.getElementById("diagram-canvas-container"); + + if (!node?.firstChild) return; + + domToImage + .toPng(node.firstChild) + .then(function (dataUrl: string) { + const link = document.createElement("a"); + link.download = `${workflowName}.png`; + link.href = dataUrl; + link.click(); + }) + .catch(function (error: Error) { + console.error("Error saving image:", error); + }); +}; + +interface ViewportProps { + viewportRef: Ref; + cursor: string; + isInconsistent: boolean; + children: ReactNode; +} + +const Viewport: FunctionComponent = ({ + viewportRef, + cursor, + isInconsistent, + children, +}) => { + const { mode } = useContext(ColorModeContext); + const darkMode = mode === "dark"; + const backgroundStyle = { + backgroundColor: darkMode ? "#000000" : "#FFFFFF", + backgroundImage: `url('/diagramDotBg.svg')`, + }; + + return ( + + {children} + + ); +}; +interface PanAndZoomWrapperProps { + isInconsistent: boolean; + panAndZoomActor: ActorRef; + leftPanelExpanded: boolean; // TODO this has to be in xstate. + viewPortChildren?: ReactNode; + children: ReactNode; + flowActor: ActorRef; + isExecutionView?: boolean; +} + +const PanAndZoomWrapper: FunctionComponent = ({ + isInconsistent, + panAndZoomActor, + children, + leftPanelExpanded, // TODO this has to be in xstate. + viewPortChildren = null, + flowActor, + isExecutionView = false, +}) => { + const [ + { zoom, canvasSize, layout, position, panEnabled, isSearchFieldVisible }, + { + handleResetZoomPosition, + handleSetPosition, + handleCenterOnSelectedTask, + handleSetInitialViewportOffset, + handleSetFitScreen, + handleZoom, + handleDrag, + handleTogglePan, + handleToggleSearchField, + handleSetZoomAndPosition, + }, + ] = usePanAndZoomActor(panAndZoomActor); + + const workflowName = useSelector(flowActor, selectWorkflowName); + + const viewportRef = useRef(null); + + const getRelativeCursorPosition = useCallback((event: WheelEvent) => { + // Get current cursor position with the viewportRef + const rect = viewportRef?.current?.getBoundingClientRect(); + + return { + x: event.clientX - (rect?.left ?? 0), + y: event.clientY - (rect?.top ?? 0), + }; + }, []); + + const resetPosition = useCallback(() => { + if (canvasSize.height > 0 && viewportRef?.current) { + const { offsetWidth, offsetHeight } = viewportRef.current; + + handleResetZoomPosition(offsetWidth, offsetHeight); + } + }, [canvasSize, viewportRef, handleResetZoomPosition]); + + const centerPosition = useCallback(() => { + if (viewportRef?.current) { + const { offsetWidth, offsetHeight } = viewportRef.current; + + handleCenterOnSelectedTask(offsetWidth, offsetHeight); + } + }, [handleCenterOnSelectedTask, viewportRef]); + + useEffect(() => { + if (viewportRef?.current) { + const { offsetWidth, offsetHeight } = viewportRef.current; + + handleSetInitialViewportOffset(offsetWidth, offsetHeight); + } + }, [handleSetInitialViewportOffset, viewportRef]); + + useEffect(() => { + centerPosition(); + }, [leftPanelExpanded, centerPosition]); + + usePinch( + ({ offset: [factor], event }: any) => { + event.stopPropagation(); + // This event needs to send the position of the mouse in the viewport. to handle zoom there + // and should disable scroll events for a period of time. + if (!isEventReallyWheel(event)) { + const cursorPosition = getRelativeCursorPosition(event); + + handleSetZoomAndPosition( + { x: cursorPosition.x, y: cursorPosition.y }, + factor, + ); + } + }, + { + scaleBounds: { min: MIN_ZOOM, max: MAX_ZOOM }, + from: zoom, + enabled: !!layout, + target: viewportRef.current!, + eventOptions: { passive: false }, + }, + ); + + const scrollCallback = useCallback>( + ({ delta, event, metaKey, ctrlKey, direction }) => { + event.stopPropagation(); + event.preventDefault(); + + if ((metaKey || ctrlKey) && direction[1] !== 0) { + const zoomSensitivity = 0.001; // Adjust this value to control zoom sensitivity + let newZoom = zoom * (1 - event.deltaY * zoomSensitivity); + + if (newZoom < MIN_ZOOM) { + newZoom = MIN_ZOOM; + } else if (newZoom > MAX_ZOOM) { + newZoom = MAX_ZOOM; + } + + const cursorPosition = getRelativeCursorPosition(event); + + handleSetZoomAndPosition( + { x: cursorPosition.x as number, y: cursorPosition.y }, + newZoom, + ); + } else { + const newX = position.x - delta[0]; + const newY = position.y - delta[1]; + handleSetPosition!({ x: newX, y: newY }); + } + }, + [ + getRelativeCursorPosition, + handleSetZoomAndPosition, + handleSetPosition, + zoom, + position, + ], + ); + + useWheel(scrollCallback, { + enabled: !!layout, + target: viewportRef.current!, + eventOptions: { passive: false }, + }); + + useDrag( + (props: any) => { + const { delta, event, tap } = props; + event.stopPropagation(); + const newX = position.x + delta[0]; + const newY = position.y + delta[1]; + + // Filter to prevent onClick event + if (!tap) { + handleDrag( + { x: newX, y: newY }, + { x: event.clientX, y: event.clientY }, + ); + } + }, + { + target: viewportRef.current!, + eventOptions: { passive: false }, + filterTaps: true, + }, + ); + + const fitToScreen = useCallback(() => { + if (viewportRef?.current) { + const { offsetWidth, offsetHeight } = viewportRef.current; + + handleSetFitScreen(offsetWidth, offsetHeight); + } + }, [viewportRef, handleSetFitScreen]); + + return ( + + + printScreen(workflowName || "workflow_diagram"), + }} + togglePan={handleTogglePan} + panEnabled={panEnabled} + flowActor={flowActor} + isSearchFieldVisible={isSearchFieldVisible} + toggleSearchField={handleToggleSearchField} + isExecutionView={isExecutionView} + /> + {viewPortChildren} +
    +
    +
    + {children} +
    +
    +
    +
    +
    + ); +}; + +export default PanAndZoomWrapper; diff --git a/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/SearchBox.tsx b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/SearchBox.tsx new file mode 100644 index 0000000000..409ec8bcf4 --- /dev/null +++ b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/SearchBox.tsx @@ -0,0 +1,96 @@ +import { Box } from "@mui/material"; +import { FlowEvents, useFlowMachine } from "components/flow/state"; + +import ClickAwayListener from "@mui/material/ClickAwayListener"; + +import { FunctionComponent, useMemo, useState } from "react"; + +import { ActorRef } from "xstate"; +import { usePanAndZoomActor } from "./state"; +import { AdvancedSearchFieldPopper } from "components/v1/AdvancedSearchFieldPopper"; +import { isPseudoTask } from "utils/utils"; +import { NodeData } from "reaflow"; +import { useHotkeys } from "react-hotkeys-hook"; +import { Key } from "ts-key-enum"; + +interface SearchBoxProps { + flowActor: ActorRef; + anchorEl: any; +} + +export const SearchBox: FunctionComponent = ({ + flowActor, + anchorEl, +}) => { + const [{ selectNode }, { nodes, panAndZoomActor }] = + useFlowMachine(flowActor); + const [ + { viewportSize }, + { handleToggleSearchField, handleSelectSearchResult }, + ] = usePanAndZoomActor(panAndZoomActor); + + const [filteredOptionsCount, setFilteredOptionsCount] = useState(0); + const [hoveredItem, setHoveredItem] = useState(""); + const [searchTerm, setSearchTerm] = useState(""); + + const suggestions = nodes.reduce( + ( + accumulator: { taskName: string; taskRef: string; type: string }[], + item: NodeData, + ) => { + if (item.data.task && !isPseudoTask(item.data.task)) { + accumulator.push({ + taskName: item.text, + taskRef: item.id, + type: item.data.task.type ?? "", + }); + } + return accumulator; + }, + [], + ); + + const filteredOptions = useMemo(() => { + if (suggestions) { + const newFilteredOptions = suggestions.filter((option) => + `${option.taskName}${option.taskRef}${option.type}` + .toLowerCase() + .includes(searchTerm.toLowerCase()), + ); + return newFilteredOptions; + } else return []; + }, [suggestions, searchTerm]); + + const handleClickSearchResult = (val: string | null) => { + const [selectedTask] = nodes.filter((item) => item.id === val); + if (selectedTask) { + selectNode(selectedTask); + handleSelectSearchResult(viewportSize?.width, viewportSize?.height); + } + }; + + useHotkeys(Key.Escape, handleToggleSearchField, { + enableOnFormTags: ["INPUT"], + }); + + return ( + + + + + + ); +}; diff --git a/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/ZoomControls.tsx b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/ZoomControls.tsx new file mode 100644 index 0000000000..3ef1f21c5d --- /dev/null +++ b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/ZoomControls.tsx @@ -0,0 +1,294 @@ +import HelpOutlineIcon from "@mui/icons-material/HelpOutline"; +import PrintOutlinedIcon from "@mui/icons-material/PrintOutlined"; +import { Box, Button, Stack } from "@mui/material"; +import { useSelector } from "@xstate/react"; +import { FlowActionTypes, FlowEvents } from "components/flow/state"; +import CustomTooltip from "pages/definition/EditorPanel/CustomTooltip"; +import { + DefinitionMachineEventTypes, + WorkflowDefinitionEvents, + WorkflowEditContext, +} from "pages/definition/state"; +import { + FunctionComponent, + RefObject, + useCallback, + useContext, + useRef, +} from "react"; +import FitToFrame from "shared/icons/FitToFrame"; +import { ZoomControlsButton } from "shared/ZoomControlsButton"; +import { ColorModeContext } from "theme/material/ColorModeContext"; +import { colors } from "theme/tokens/variables"; +import { logrocketTrackIfEnabled } from "utils/logrocket"; +import { ActorRef } from "xstate"; +import { MAX_ZOOM } from "./constants"; +import DragNDrop from "./icons/DragNDrop"; +import Home from "./icons/Home"; +import Minus from "./icons/Minus"; +import Plus from "./icons/Plus"; +import Search from "./icons/Search"; +import { SearchBox } from "./SearchBox"; + +export interface ZoomControlsProps { + zoom: number; + setZoom: (zoomIn: boolean) => void; + resetPosition: () => void; + isInconsistent: boolean; + fitToScreen: () => void; + togglePan: () => void; + panEnabled: boolean; + flowActor: ActorRef; + isSearchFieldVisible: boolean; + toggleSearchField: () => void; + printScreen: () => void; + isExecutionView: boolean; +} +// FIXME this should not be here since we are coupling to the definition machine.. +// ONCE dillip confirms move it elsewhere. +const MaybeCoolTooltip = ({ + actor, + anchorEl, +}: { + actor: ActorRef; + anchorEl: RefObject; +}) => { + const shouldShowTooltip = useSelector(actor, (state) => + state.hasTag("showDescriptionTooltip"), + ); + const handleNextButtonClick = useCallback(() => { + actor.send({ + type: DefinitionMachineEventTypes.NEXT_STEP_IMPORT_SUCCESSFUL_DIALOG, + }); + }, [actor]); + + const handleDismissTutorial = () => { + actor.send(DefinitionMachineEventTypes.DISMISS_IMPORT_SUCCESSFUL_DIALOG); + }; + + return shouldShowTooltip ? ( + + + + Diagram controls + + + + Use diagram controls to show task descriptions, Change zoom + settings, Drag tasks arround and more. + + + + + + } + /> + ) : null; +}; + +export const ZoomControls: FunctionComponent = ({ + zoom, + setZoom, + resetPosition, + isInconsistent, + fitToScreen, + togglePan, + panEnabled, + flowActor, + isSearchFieldVisible, + toggleSearchField, + printScreen, + isExecutionView, +}) => { + const { mode } = useContext(ColorModeContext); + const workflowEditContext = useContext(WorkflowEditContext); + const darkMode = mode === "dark"; + const zoomPercent = Math.round(zoom * 100); + const borderColor = darkMode ? colors.gray04 : colors.lightGrey; + + const anchorRef = useRef(null); + const showDescriptionButtonRef = useRef(null); + const disableZoomIn = zoom >= MAX_ZOOM; + + const isShowDescription = useSelector(flowActor, (state) => + state.hasTag("showDescription"), + ); + const handleToggleShowDescription = useCallback(() => { + flowActor.send({ type: FlowActionTypes.TOGGLE_SHOW_DESCRIPTION }); + logrocketTrackIfEnabled("user_toggle_show_description"); + }, [flowActor]); + + return ( + + { + resetPosition(); + }} + disabled={isInconsistent} + tooltip="Reset position" + > + + + + {zoomPercent}% + + { + setZoom(true); + }} + disabled={isInconsistent} + tooltip="Zoom out" + > + + + { + setZoom(false); + }} + style={{ + borderLeft: `1px solid ${borderColor}`, + }} + disabled={isInconsistent || disableZoomIn} + tooltip="Zoom in" + > + + + + + + {!isExecutionView && ( + togglePan()} + disabled={isInconsistent} + tooltip={`${panEnabled ? "Enable" : "Disable"} dragging mode`} + > + + + )} + { + printScreen(); + }} + tooltip="Export to image" + > + + + + + + + + + + + + {isSearchFieldVisible && ( + + )} + {workflowEditContext.workflowDefinitionActor != null ? ( + + ) : null} + + ); +}; diff --git a/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/constants.ts b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/constants.ts new file mode 100644 index 0000000000..10b68e6a92 --- /dev/null +++ b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/constants.ts @@ -0,0 +1,6 @@ +export const MIN_ZOOM = 0.02; +export const MAX_ZOOM = 2; + +export const INITIAL_ZOOM = 0.75; + +export const EDGE_SPACING = 170; diff --git a/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/icons/DragNDrop.tsx b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/icons/DragNDrop.tsx new file mode 100644 index 0000000000..862a4cecda --- /dev/null +++ b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/icons/DragNDrop.tsx @@ -0,0 +1,18 @@ +function Icon({ size = "20", color = "#000000" }) { + return ( + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/icons/Home.tsx b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/icons/Home.tsx new file mode 100644 index 0000000000..5576fb9fd2 --- /dev/null +++ b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/icons/Home.tsx @@ -0,0 +1,18 @@ +function Icon({ size = "20", color = "#000000" }) { + return ( + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/icons/Minus.tsx b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/icons/Minus.tsx new file mode 100644 index 0000000000..8baceb177c --- /dev/null +++ b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/icons/Minus.tsx @@ -0,0 +1,18 @@ +function Icon({ size = "20", color = "#000000" }) { + return ( + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/icons/Plus.tsx b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/icons/Plus.tsx new file mode 100644 index 0000000000..0472e32fc2 --- /dev/null +++ b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/icons/Plus.tsx @@ -0,0 +1,18 @@ +function Icon({ size = "20", color = "#000000" }) { + return ( + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/icons/Search.tsx b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/icons/Search.tsx new file mode 100644 index 0000000000..cdc979c455 --- /dev/null +++ b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/icons/Search.tsx @@ -0,0 +1,18 @@ +function Icon({ size = "20", color = "#000000" }) { + return ( + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/index.ts b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/index.ts new file mode 100644 index 0000000000..bc68f36df8 --- /dev/null +++ b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/index.ts @@ -0,0 +1,3 @@ +import PanAndZoomWrapper from "./PanAndZoomWrapper"; +export * from "./state"; +export default PanAndZoomWrapper; diff --git a/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/state/actions.ts b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/state/actions.ts new file mode 100644 index 0000000000..901bf91316 --- /dev/null +++ b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/state/actions.ts @@ -0,0 +1,273 @@ +import { assign, raise } from "xstate"; +import _isNaN from "lodash/isNaN"; + +import { + CenterOnSelectedTaskEvent, + HandleZoomEvent, + PanAndZoomMachineContext, + ResetZoomPositionEvent, + SelectNodeEvent, + SetFitScreenEvent, + SetInitialViewportOffsetEvent, + SetLayoutEvent, + SetPositionEvent, + SetZoomEvent, + SetZoomToPositionEvent, + DragEvent, + PanAndZoomEventTypes, + ToggleSearchEvent, + SetNotifiedEventTypeEvent, +} from "./types"; + +import { MAX_ZOOM, MIN_ZOOM } from "../constants"; +import { ZOOMING_STEP } from "utils/constants/workflow"; +import { + applyZoomToCursor, + calculateZoomPosition, + centerInBestLayoutNode, + initialZoomCenter, + NodeWithSizeAndPosition, +} from "./helpers"; +import { featureFlags, FEATURES } from "utils"; + +const DRAG_DROP_TASK_INCREMENT_THRESHOLD = featureFlags.getValue( + FEATURES.DRAG_DROP_TASK_INCREMENT_THRESHOLD, +); + +export const resetZoomPosition = assign< + PanAndZoomMachineContext, + ResetZoomPositionEvent +>( + ( + { layout, viewportSize, zoom }, + { viewportOffsetWidth, viewportOffsetHeight }, + ) => + initialZoomCenter({ + layout, + viewportOffsetWidth: viewportOffsetWidth || viewportSize.width, + viewportOffsetHeight: viewportOffsetHeight || viewportSize.height, + zoom, + }), +); + +export const setLayout = assign( + (context, { layout }) => { + const canvasWidth = layout?.width || context.canvasSize.width; + const canvasHeight = layout?.height || context.canvasSize.height; + return { + canvasSize: { width: canvasWidth, height: canvasHeight }, + layout: layout, + // viewportSize:{} + }; + }, +); + +export const setZoom = assign( + (context, { zoom }) => { + return calculateZoomPosition({ context, newZoom: zoom }); + }, +); + +export const setPosition = assign({ + position: (__context, { position }) => position, +}); + +export const setSelectedNode = assign< + PanAndZoomMachineContext, + SelectNodeEvent +>({ + selectedNode: (_context, { node }) => node, +}); + +export const setInitialViewportOffset = assign< + PanAndZoomMachineContext, + SetInitialViewportOffsetEvent +>((_, { viewportOffsetWidth, viewportOffsetHeight }) => ({ + lastViewportOffsetWidth: viewportOffsetWidth, + lastViewportOffsetHeight: viewportOffsetHeight, + viewportSize: { width: viewportOffsetWidth, height: viewportOffsetHeight }, +})); + +export const centerUsingContext = assign( + ({ + layout, + lastViewportOffsetWidth: viewportOffsetWidth, + lastViewportOffsetHeight: viewportOffsetHeight, + zoom, + }) => + initialZoomCenter({ + layout, + viewportOffsetWidth: viewportOffsetWidth!, + viewportOffsetHeight: viewportOffsetHeight!, + zoom, + }), +); + +export const centerOnSelectedTask = assign< + PanAndZoomMachineContext, + CenterOnSelectedTaskEvent +>((context, { viewportOffsetWidth, viewportOffsetHeight }) => { + if (context.layout) { + const { layout, position, selectedNode, zoom } = context; + + const widthToUse = viewportOffsetWidth || context.lastViewportOffsetWidth!; + const heightToUse = + viewportOffsetHeight || context.lastViewportOffsetHeight!; + + const newPosition = centerInBestLayoutNode( + layout?.children || [], + { width: widthToUse, height: heightToUse }, + zoom, + selectedNode as NodeWithSizeAndPosition, + ); + + if (newPosition === null) { + return initialZoomCenter({ + layout, + viewportOffsetWidth: widthToUse, + viewportOffsetHeight: heightToUse, + zoom, + }); + } + + const { x: positionX, y: positionY } = newPosition || context.position; + + return { + position: { + x: _isNaN(positionX) ? (widthToUse - layout!.width!) / 2 : positionX, + y: _isNaN(positionY) ? position.y : positionY, + }, + lastViewportOffsetWidth: widthToUse, + lastViewportOffsetHeight: heightToUse, + viewportSize: { width: widthToUse, height: heightToUse }, + }; + } + + return context; +}); + +export const fitToScreen = assign( + (context, { viewportOffsetWidth, viewportOffsetHeight }) => { + const { layout } = context; + + // Calculate the scale ratio for both width and height + const widthRatio = layout?.width ? viewportOffsetWidth / layout.width : 1; + const heightRatio = layout?.height + ? viewportOffsetHeight / layout.height + : 1; + // Use the smaller ratio to fit the canvas into the viewport + const scale = Math.min(widthRatio, heightRatio); + + // Calculate the new diagram width and height + const newDiagramWidth = (layout?.width || 1) * scale; + const newDiagramHeight = (layout?.height || 1) * scale; + + // Calculate the position of the diagram in the viewport + const positionX = Math.ceil( + widthRatio === scale ? 0 : (viewportOffsetWidth - newDiagramWidth) / 2, + ); + const positionY = Math.ceil((viewportOffsetHeight - newDiagramHeight) / 2); + + return { + position: { + x: positionX, + y: positionY, + }, + zoom: scale, + }; + }, +); + +export const setZoomToPosition = assign< + PanAndZoomMachineContext, + SetZoomToPositionEvent +>((context, { zoom, position }) => { + const currentPosition = context.position; + const oldZoom = context.zoom; + + return applyZoomToCursor(currentPosition, position, oldZoom, zoom); +}); + +export const handleZoom = assign( + (context, { isZoomOut }) => { + const roundedContextZoom = Math.round(context.zoom * 10) / 10; + const newZoom = isZoomOut + ? roundedContextZoom - ZOOMING_STEP + : roundedContextZoom + ZOOMING_STEP; + + if (isZoomOut && newZoom > MIN_ZOOM) { + return calculateZoomPosition({ context, newZoom }); + } + if (!isZoomOut && newZoom <= MAX_ZOOM) { + return calculateZoomPosition({ context, newZoom }); + } + + return context; + }, +); + +const INCREMENT_THRESHOLD = isNaN(DRAG_DROP_TASK_INCREMENT_THRESHOLD) + ? 10 + : Number(DRAG_DROP_TASK_INCREMENT_THRESHOLD); + +const MIN_ALLOWED_WIDTH = 210; // Estimated from the menu bar to the left +const MIN_ALLOWED_HEIGHT = 180; // Estimated from the menu bar to the top +export const setPositionOfDraggingTask = assign< + PanAndZoomMachineContext, + DragEvent +>((context, { clientMousePosition }) => { + let draggingUpdatedPosition = context.draggingUpdatedPosition; + const maxAllowedWidth = context.lastViewportOffsetWidth! - 10; + + const maxAllowedHeight = context.lastViewportOffsetHeight! - 100; + + const currentPosition = { ...context.position }; + + if (clientMousePosition.x >= maxAllowedWidth) { + currentPosition.x = context.position.x - INCREMENT_THRESHOLD; + draggingUpdatedPosition = true; + } + + if (clientMousePosition.x <= MIN_ALLOWED_WIDTH) { + currentPosition.x = context.position.x + INCREMENT_THRESHOLD; + + draggingUpdatedPosition = true; + } + + if (clientMousePosition.y >= maxAllowedHeight) { + currentPosition.y = context.position.y - INCREMENT_THRESHOLD; + + draggingUpdatedPosition = true; + } + + if (clientMousePosition.y <= MIN_ALLOWED_HEIGHT) { + // Note this represents the top of the screen + currentPosition.y = context.position.y + INCREMENT_THRESHOLD; + + draggingUpdatedPosition = true; + } + return { + position: currentPosition, + draggingUpdatedPosition, + }; +}); +export const cleanUpPositionUpdatedFlag = assign({ + draggingUpdatedPosition: false, +}); + +export const fireToggleSearchField = raise< + PanAndZoomMachineContext, + ToggleSearchEvent +>( + { + type: PanAndZoomEventTypes.TOGGLE_SEARCH_EVT, + }, + { delay: 200 }, +); + +export const setNotifiedEventType = assign< + PanAndZoomMachineContext, + SetNotifiedEventTypeEvent +>((__, event) => { + return { notifiedEventType: event.eventType }; +}); diff --git a/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/state/context.tsx b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/state/context.tsx new file mode 100644 index 0000000000..787ac71b62 --- /dev/null +++ b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/state/context.tsx @@ -0,0 +1,12 @@ +import { createContext, ReactNode } from "react"; +import { ActorRef } from "xstate"; +import { PanAndZoomEvents } from "./types"; + +export interface PanAndZoomContextProps { + panAndZoomActor?: ActorRef; + children?: ReactNode; +} + +export const PanAndZoomContext = createContext({ + panAndZoomActor: undefined, +}); diff --git a/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/state/helpers.test.ts b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/state/helpers.test.ts new file mode 100644 index 0000000000..1213867355 --- /dev/null +++ b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/state/helpers.test.ts @@ -0,0 +1,76 @@ +import { applyZoomToCursor } from "./helpers"; + +const zoomCases = [ + { + description: "Zoom out with same cursor position", + currentPosition: { x: 100, y: 100 }, + cursorPosition: { x: 100, y: 100 }, + oldZoom: 0.5, + newZoom: 0.6, + expected: { + position: { + x: 100, + y: 100, + }, + zoom: 0.6, + }, + }, + { + description: "Zoom out with different cursor position", + currentPosition: { x: 100, y: 100 }, + cursorPosition: { x: 200, y: 200 }, + oldZoom: 0.5, + newZoom: 0.6, + expected: { + position: { + x: 80, + y: 80, + }, + zoom: 0.6, + }, + }, + { + description: "Zoom in with same cursor position", + currentPosition: { x: 100, y: 100 }, + cursorPosition: { x: 100, y: 100 }, + oldZoom: 0.5, + newZoom: 0.4, + expected: { + position: { + x: 100, + y: 100, + }, + zoom: 0.4, + }, + }, + { + description: "Zoom in with different cursor position", + currentPosition: { x: 100, y: 100 }, + cursorPosition: { x: 200, y: 200 }, + oldZoom: 0.5, + newZoom: 0.4, + expected: { + position: { + x: 120, + y: 120, + }, + zoom: 0.4, + }, + }, +]; + +describe("Testing applyZoomToCursor function", () => { + test.each(zoomCases)( + "Testing $description: given $oldZoom and $newZoom as arguments, returns $expected", + ({ oldZoom, newZoom, currentPosition, cursorPosition, expected }) => { + const result = applyZoomToCursor( + currentPosition, + cursorPosition, + oldZoom, + newZoom, + ); + + expect(result).toMatchObject(expected); + }, + ); +}); diff --git a/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/state/helpers.ts b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/state/helpers.ts new file mode 100644 index 0000000000..b07075c5be --- /dev/null +++ b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/state/helpers.ts @@ -0,0 +1,180 @@ +import { ElkRoot, NodeData } from "reaflow"; +import { PanAndZoomMachineContext, PositionProps, SizeProps } from "./types"; + +type CenterParams = { + layout?: ElkRoot; + viewportOffsetWidth: number; + viewportOffsetHeight: number; + zoom: number; +}; + +type SizeAndPosition = PositionProps & { width: number; height: number }; + +export const PADDING_TOP = 65; + +export const centerCanvasToNodePosition = ( + containerSize: SizeProps, + node: SizeAndPosition, + scale: number, +) => { + // Calculate position of the canvas to center at X coordinate + const viewPortCenterX = containerSize.width / 2; + const realXPosition = node.width / 2 + node.x; // X coordinate of the node plus half of the node width + const scaledXCoordinate = realXPosition * scale; // Scale X coordinate + const positionX = viewPortCenterX - scaledXCoordinate; // Center of the viewport minus the scaled X coordinate + + const viewportCenterY = containerSize.height / 2; + const realYPosition = node.height / 2 + node.y; // Y coordinate of the node plus half of the node height + const scaledYCoordinate = realYPosition * scale; // Scale Y coordinate + const positionY = viewportCenterY - scaledYCoordinate; // Center of the viewport minus the scaled Y coordinate + + return { + x: positionX, + y: positionY, + }; +}; + +export type NodeWithSizeAndPosition = NodeData & + SizeAndPosition & { children?: NodeWithSizeAndPosition[] }; + +export const centerInBestLayoutNode = ( + children: NodeWithSizeAndPosition[], + containerSize: SizeProps, + scale: number, + selectedNode?: NodeWithSizeAndPosition, +): SizeAndPosition | undefined => { + // No children. then nothing to do. + if (children.length === 0 || selectedNode == null) return undefined; + + // If no selected node center somewhere + const nodeSelected = selectedNode; //|| _first(children)!; + + for (const node of children) { + if (node.id === nodeSelected.id) { + return { + ...centerCanvasToNodePosition(containerSize, node, scale), // Node found cool center according to parameters + width: node.width, + height: node.height, + }; + } + // Node not was not found but has children look for childs + if (node.children) { + const result = centerInBestLayoutNode( + // the node has to be centered relative to its container so in this case the paren is the container + node.children, + node, + 1, + selectedNode, + ); + if (result) { + // result was found + const resultPosition = centerCanvasToNodePosition( + // Center using our real container size + containerSize, + { + x: node.x - result.x, // we move inside our new container according to the result of the previous center + y: node.y - result.y, + width: node.width, + height: node.height, + }, + scale, + ); + return { + ...resultPosition, + width: node.width, + height: node.height, + }; + } + } + } +}; + +export const initialZoomCenter = ({ + layout, + viewportOffsetWidth, + viewportOffsetHeight, + zoom, +}: CenterParams): Partial => { + const startNode = layout?.children?.[0]; + + if (!startNode) { + return {}; + } + + const centerPosition = centerCanvasToNodePosition( + { + width: viewportOffsetWidth, + height: viewportOffsetHeight, + }, + startNode, + zoom, + ); + + return { + position: { + x: centerPosition.x, + // Padding top & control bar height (40) + y: startNode.y + PADDING_TOP, + }, + zoom, + viewportSize: { width: viewportOffsetWidth, height: viewportOffsetHeight }, + lastViewportOffsetWidth: viewportOffsetWidth, + lastViewportOffsetHeight: viewportOffsetHeight, + }; +}; + +export const applyZoomToCursor = ( + currentPosition: { x: number; y: number }, + cursorPosition: { x: number; y: number }, + oldZoom: number, + newZoom: number, +) => { + // Calculate the change in zoom + const zoomFactor = newZoom / oldZoom; + + // Calculate the new position to keep the cursor in the same position relative to the canvas content + const deltaX = (cursorPosition.x - currentPosition.x) * (1 - zoomFactor); + const deltaY = (cursorPosition.y - currentPosition.y) * (1 - zoomFactor); + + return { + position: { + x: currentPosition.x + deltaX, + y: currentPosition.y + deltaY, + }, + zoom: newZoom, + }; +}; + +export const calculateZoomPosition = ({ + context, + newZoom, +}: { + context: PanAndZoomMachineContext; + newZoom: number; +}) => { + const { layout, position: currentPosition, zoom: oldZoom } = context; + + // Strategy: + // Try to keep the position Y that will make the diagram zoom in/out center of X + + // Old center position (C0) + const oldCenterX = (layout?.width ?? 0 * oldZoom) / 2; + // const oldCenterY = (layout?.height! * oldZoom) / 2; + + // New center position (C1) + const newCenterX = (layout?.width ?? 0 * newZoom) / 2; + // const newCenterY = (layout?.height! * newZoom) / 2; + + // Delta + const deltaX = oldCenterX - newCenterX; + // const deltaY = oldCenterY - newCenterY; + + return { + zoom: newZoom, + position: { + x: currentPosition.x + deltaX, + // if you need to shrink/expand to the center => + deltaY + y: currentPosition.y, + }, + }; +}; diff --git a/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/state/hook.ts b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/state/hook.ts new file mode 100644 index 0000000000..278e11d654 --- /dev/null +++ b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/state/hook.ts @@ -0,0 +1,194 @@ +import { useCallback } from "react"; +import { useSelector } from "@xstate/react"; +import { ActorRef } from "xstate"; +import { + PanAndZoomEvents, + PanAndZoomEventTypes, + PositionProps, + PanAndZoomStates, +} from "./types"; + +export const usePanAndZoomActor = ( + panAndZoomActor: ActorRef, +) => { + const send = panAndZoomActor.send; + + const handleResetZoomPosition = useCallback( + (viewportOffsetWidth: number, viewportOffsetHeight: number) => { + send({ + type: PanAndZoomEventTypes.RESET_ZOOM_POSITION_EVT, + viewportOffsetWidth, + viewportOffsetHeight, + }); + }, + [send], + ); + + const handleSetZoom = useCallback( + (zoom: number) => { + send({ type: PanAndZoomEventTypes.SET_ZOOM_EVT, zoom }); + }, + [send], + ); + + const handleSetPosition = useCallback( + (position: PositionProps) => { + send({ type: PanAndZoomEventTypes.SET_POSITION_EVT, position }); + }, + [send], + ); + + const handleDrag = useCallback( + (position: PositionProps, clientMousePosition: PositionProps) => { + send({ + type: PanAndZoomEventTypes.DRAG_EVENT_EVT, + position, + clientMousePosition, + }); + }, + [send], + ); + + const handleCenterOnSelectedTask = useCallback( + (viewportOffsetWidth: number, viewportOffsetHeight: number) => { + send({ + type: PanAndZoomEventTypes.CENTER_ON_SELECTED_TASK, + viewportOffsetWidth, + viewportOffsetHeight, + }); + }, + [send], + ); + + const handleSetFullScreen = useCallback( + (fullScreen: boolean, viewportOffsetWidth: number) => { + send({ + type: PanAndZoomEventTypes.SET_FULL_SCREEN_EVT, + viewportOffsetWidth, + fullScreen, + }); + }, + [send], + ); + + const handleSetFitScreen = useCallback( + (viewportOffsetWidth: number, viewportOffsetHeight: number) => { + send({ + type: PanAndZoomEventTypes.SET_FIT_SCREEN_EVT, + viewportOffsetWidth, + viewportOffsetHeight, + }); + }, + [send], + ); + + const handleSetInitialViewportOffset = useCallback( + (viewportOffsetWidth: number, viewportOffsetHeight: number) => { + send({ + type: PanAndZoomEventTypes.SET_INITIAL_VIEWPORT_OFFSET, + viewportOffsetWidth, + viewportOffsetHeight, + }); + }, + [send], + ); + + const handleZoom = useCallback( + (isZoomOut: boolean) => { + send({ + type: PanAndZoomEventTypes.HANDLE_ZOOM_EVT, + isZoomOut, + }); + }, + [send], + ); + + const handleTogglePan = useCallback( + () => send({ type: PanAndZoomEventTypes.TOGGLE_PAN_EVT }), + [send], + ); + + const handleSetZoomAndPosition = useCallback( + (position: PositionProps, zoom: number) => + send({ + type: PanAndZoomEventTypes.SET_ZOOM_TO_POSITION_EVT, + zoom, + position, + }), + [send], + ); + + const handleToggleSearchField = useCallback( + () => send({ type: PanAndZoomEventTypes.TOGGLE_SEARCH_EVT }), + [send], + ); + + const handleSelectSearchResult = useCallback( + (viewportOffsetWidth: number, viewportOffsetHeight: number) => + send({ + type: PanAndZoomEventTypes.SELECT_SEARCH_RESULT, + viewportOffsetWidth, + viewportOffsetHeight, + }), + [send], + ); + + const handleSetEventType = useCallback( + (eventType: string) => + send({ type: PanAndZoomEventTypes.SET_NOTIFIED_EVENT_TYPE, eventType }), + [send], + ); + + return [ + { + zoom: useSelector(panAndZoomActor, (state) => state.context.zoom), + canvasSize: useSelector( + panAndZoomActor, + (state) => state.context.canvasSize, + ), + layout: useSelector(panAndZoomActor, (state) => state.context.layout), + position: useSelector(panAndZoomActor, (state) => state.context.position), + panEnabled: useSelector(panAndZoomActor, (state) => + state.matches([ + PanAndZoomStates.IDLE, + PanAndZoomStates.PAN, + PanAndZoomStates.PAN_ENABLED, + ]), + ), + viewportSize: useSelector( + panAndZoomActor, + (state) => state.context.viewportSize, + ), + isSearchFieldVisible: useSelector(panAndZoomActor, (state) => + state.matches([ + PanAndZoomStates.IDLE, + PanAndZoomStates.SEARCH_FIELD, + PanAndZoomStates.SEARCH_FIELD_VISIBLE, + ]), + ), + isPanAndZoomIdle: useSelector(panAndZoomActor, (state) => + state.matches([PanAndZoomStates.IDLE]), + ), + notifiedEventType: useSelector( + panAndZoomActor, + (state) => state.context.notifiedEventType, + ), + }, + { + handleResetZoomPosition, + handleSetZoom, + handleSetPosition, + handleCenterOnSelectedTask, + handleSetInitialViewportOffset, + handleSetFullScreen, + handleSetFitScreen, + handleZoom, + handleTogglePan, + handleDrag, + handleSetZoomAndPosition, + handleToggleSearchField, + handleSelectSearchResult, + handleSetEventType, + }, + ] as const; +}; diff --git a/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/state/index.ts b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/state/index.ts new file mode 100644 index 0000000000..c5a88e2ce2 --- /dev/null +++ b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/state/index.ts @@ -0,0 +1,4 @@ +export * from "./hook"; +export * from "./machine"; +export * from "./types"; +export * from "./context"; diff --git a/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/state/machine.ts b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/state/machine.ts new file mode 100644 index 0000000000..accd9e0962 --- /dev/null +++ b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/state/machine.ts @@ -0,0 +1,170 @@ +import { createMachine } from "xstate"; +import * as actions from "./actions"; +import _isEmpty from "lodash/isEmpty"; +import { + PanAndZoomMachineContext, + PanAndZoomEventTypes, + PanAndZoomEvents, + PanAndZoomStates, +} from "./types"; +import { INITIAL_ZOOM } from "../constants"; + +const NO_SIZE = { width: 0, height: 0 }; +const INITIAL_POSITION = { x: 0, y: 0 }; + +export const panAndZoomMachine = createMachine< + PanAndZoomMachineContext, + PanAndZoomEvents +>( + { + id: "panAndZoomMachine", + predictableActionArguments: true, + initial: PanAndZoomStates.INIT, + context: { + zoom: INITIAL_ZOOM, + canvasSize: NO_SIZE, + viewportSize: NO_SIZE, + position: INITIAL_POSITION, + isFullScreen: false, + draggingUpdatedPosition: false, + notifiedEventType: "", + }, + states: { + [PanAndZoomStates.INIT]: { + on: { + [PanAndZoomEventTypes.SET_LAYOUT_EVT]: { + actions: "setLayout", + target: "checkIfReady", + }, + [PanAndZoomEventTypes.SET_INITIAL_VIEWPORT_OFFSET]: { + actions: "setInitialViewportOffset", + target: "checkIfReady", + }, + }, + }, + [PanAndZoomStates.CHECK_IF_READY]: { + always: [ + { + target: PanAndZoomStates.INIT, + cond: ({ layout, lastViewportOffsetWidth }) => + _isEmpty(layout?.children) || lastViewportOffsetWidth == null, + }, + { actions: "resetZoomPosition", target: PanAndZoomStates.IDLE }, + ], + }, + [PanAndZoomStates.IDLE]: { + on: { + [PanAndZoomEventTypes.RESET_ZOOM_POSITION_EVT]: { + actions: "resetZoomPosition", + }, + [PanAndZoomEventTypes.SET_ZOOM_EVT]: { + actions: "setZoom", + }, + [PanAndZoomEventTypes.SET_FIT_SCREEN_EVT]: { + actions: ["fitToScreen"], + }, + [PanAndZoomEventTypes.HANDLE_ZOOM_EVT]: { + actions: ["handleZoom"], + }, + [PanAndZoomEventTypes.SET_ZOOM_TO_POSITION_EVT]: { + actions: "setZoomToPosition", + }, + [PanAndZoomEventTypes.CENTER_ON_SELECTED_TASK]: { + actions: "centerOnSelectedTask", + }, + [PanAndZoomEventTypes.SELECT_NODE_EVENT_EVT]: { + actions: ["setSelectedNode"], + }, + [PanAndZoomEventTypes.SET_LAYOUT_EVT]: { + actions: ["setLayout"], + }, + [PanAndZoomEventTypes.SET_POSITION_EVT]: { + actions: "setPosition", + }, + [PanAndZoomEventTypes.SELECT_SEARCH_RESULT]: { + actions: ["centerOnSelectedTask", "fireToggleSearchField"], + }, + [PanAndZoomEventTypes.SET_NOTIFIED_EVENT_TYPE]: { + actions: "setNotifiedEventType", + }, + }, + type: "parallel", + states: { + [PanAndZoomStates.PAN]: { + initial: PanAndZoomStates.PAN_ENABLED, + states: { + [PanAndZoomStates.PAN_ENABLED]: { + on: { + [PanAndZoomEventTypes.DRAG_EVENT_EVT]: { + actions: ["setPosition"], + }, + [PanAndZoomEventTypes.TOGGLE_PAN_EVT]: { + target: PanAndZoomStates.PAN_DISABLED, + }, + }, + }, + [PanAndZoomStates.PAN_DISABLED]: { + on: { + [PanAndZoomEventTypes.TOGGLE_PAN_EVT]: { + target: PanAndZoomStates.PAN_ENABLED, + }, + }, + initial: PanAndZoomStates.NOT_DRAGGING_TASK, + states: { + [PanAndZoomStates.DRAGGING_TASK]: { + on: { + [PanAndZoomEventTypes.DRAG_EVENT_EVT]: { + actions: ["setPositionOfDraggingTask"], + }, + [PanAndZoomEventTypes.DRAG_TASK_END]: { + target: PanAndZoomStates.NOT_DRAGGING_TASK, + actions: ["cleanUpPositionUpdatedFlag"], + }, + }, + }, + [PanAndZoomStates.NOT_DRAGGING_TASK]: { + on: { + [PanAndZoomEventTypes.DRAG_TASK_BEGIN]: { + target: PanAndZoomStates.DRAGGING_TASK, + }, + }, + }, + }, + }, + }, + }, + [PanAndZoomStates.SEARCH_FIELD]: { + initial: PanAndZoomStates.SEARCH_FIELD_HIDDEN, + states: { + [PanAndZoomStates.SEARCH_FIELD_VISIBLE]: { + on: { + [PanAndZoomEventTypes.TOGGLE_SEARCH_EVT]: { + target: PanAndZoomStates.SEARCH_FIELD_HIDDEN, + }, + }, + }, + [PanAndZoomStates.SEARCH_FIELD_HIDDEN]: { + on: { + [PanAndZoomEventTypes.TOGGLE_SEARCH_EVT]: { + target: PanAndZoomStates.SEARCH_FIELD_VISIBLE, + }, + }, + }, + }, + }, + + // pan + // pan enabled + // pan desabled + + // searchfield + // visible + // not visible + }, + }, + }, + }, + { + actions: actions as any, + }, +); diff --git a/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/state/types.ts b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/state/types.ts new file mode 100644 index 0000000000..03a4b56bd7 --- /dev/null +++ b/ui-next/src/components/flow/components/graphs/PanAndZoomWrapper/state/types.ts @@ -0,0 +1,177 @@ +import { ElkRoot, NodeData } from "reaflow"; + +export type SizeProps = { width: number; height: number }; +export type PositionProps = { x: number; y: number }; + +export interface PanAndZoomMachineContext { + zoom: number; + canvasSize: SizeProps; + viewportSize: SizeProps; + position: PositionProps; + layout?: ElkRoot; + selectedNode?: NodeData; + lastViewportOffsetWidth?: number; + lastViewportOffsetHeight?: number; + isFullScreen: boolean; + draggingUpdatedPosition: boolean; + notifiedEventType: string; +} + +export enum PanAndZoomStates { + INIT = "init", + CHECK_IF_READY = "checkIfReady", + IDLE = "idle", + PAN_ENABLED = "panEnabled", + PAN_DISABLED = "panDisabled", + DRAGGING_TASK = "draggingTask", + NOT_DRAGGING_TASK = "notDraggingTask", + PAN = "pan", + SEARCH_FIELD = "searchField", + SEARCH_FIELD_VISIBLE = "searchFieldVisible", + SEARCH_FIELD_HIDDEN = "searchFieldHidden", +} + +export enum PanAndZoomEventTypes { + RESET_ZOOM_POSITION_EVT = "RESET_ZOOM_POSITION", + SET_LAYOUT_EVT = "SET_LAYOUT", + SET_ZOOM_EVT = "SET_ZOOM", + SET_POSITION_EVT = "SET_POSITION", + CENTER_ON_SELECTED_TASK = "CENTER_ON_SELECTED_TASK", + SELECT_NODE_EVENT_EVT = "SELECT_NODE_EVT", + SET_READ_ONLY_EVT = "SET_READ_ONLY_EVT", + SET_INITIAL_VIEWPORT_OFFSET = "SET_INITIAL_VIEWPORT_OFFSET", + SET_FULL_SCREEN_EVT = "SET_FULL_SCREEN_EVT", + SET_FIT_SCREEN_EVT = "SET_FIT_SCREEN_EVT", + HANDLE_ZOOM_EVT = "HANDLE_ZOOM_EVT", + TOGGLE_PAN_EVT = "TOGGLE_PAN_EVT", + SET_ZOOM_TO_POSITION_EVT = "SET_ZOOM_TO_POSITION_EVT", + INCREMENT_POSITION_Y_EVT = "INCREMENT_POSITION_Y_EVT", + DECREMENT_POSITION_Y_EVT = "DECREMENT_POSITION_Y_EVT", + INCREMENT_POSITION_X_EVT = "INCREMENT_POSITION_X_EVT", + DECREMENT_POSITION_X_EVT = "DECREMENT_POSITION_X_EVT", + DRAG_EVENT_EVT = "DRAG_EVENT_EVT", + + DRAG_TASK_BEGIN = "DRAG_TASK_BEGIN", + DRAG_TASK_END = "DRAG_TASK_END", + TOGGLE_SEARCH_EVT = "TOGGLE_SEARCH_EVT", + SELECT_SEARCH_RESULT = "SELECT_SEARCH_RESULT", + SET_NOTIFIED_EVENT_TYPE = "SET_NOTIFIED_EVENT_TYPE", + TOGGLE_SHOW_DESCRIPTION_EVT = "TOGGLE_SHOW_DESCRIPTION_EVT", +} + +export type ResetZoomPositionEvent = { + type: PanAndZoomEventTypes.RESET_ZOOM_POSITION_EVT; + viewportOffsetWidth: number; + viewportOffsetHeight: number; +}; + +export type SetLayoutEvent = { + type: PanAndZoomEventTypes.SET_LAYOUT_EVT; + layout: ElkRoot; +}; + +export type SetZoomEvent = { + type: PanAndZoomEventTypes.SET_ZOOM_EVT; + zoom: number; +}; + +export type SetZoomToPositionEvent = { + type: PanAndZoomEventTypes.SET_ZOOM_TO_POSITION_EVT; + zoom: number; + position: PositionProps; +}; + +export type SetPositionEvent = { + type: PanAndZoomEventTypes.SET_POSITION_EVT; + position: PositionProps; +}; + +export type DragEvent = { + type: PanAndZoomEventTypes.DRAG_EVENT_EVT; + position: PositionProps; + clientMousePosition: PositionProps; +}; + +export type CenterOnSelectedTaskEvent = { + type: PanAndZoomEventTypes.CENTER_ON_SELECTED_TASK; + viewportOffsetWidth: number; + viewportOffsetHeight: number; +}; + +export type SelectNodeEvent = { + type: PanAndZoomEventTypes.SELECT_NODE_EVENT_EVT; + node: NodeData; +}; + +export type SetInitialViewportOffsetEvent = { + type: PanAndZoomEventTypes.SET_INITIAL_VIEWPORT_OFFSET; + viewportOffsetWidth: number; + viewportOffsetHeight: number; +}; + +export type SetFullScreenEvent = { + type: PanAndZoomEventTypes.SET_FULL_SCREEN_EVT; + fullScreen: boolean; + viewportOffsetWidth: number; +}; + +export type SetFitScreenEvent = { + type: PanAndZoomEventTypes.SET_FIT_SCREEN_EVT; + viewportOffsetWidth: number; + viewportOffsetHeight: number; +}; + +export type HandleZoomEvent = { + type: PanAndZoomEventTypes.HANDLE_ZOOM_EVT; + isZoomOut: boolean; +}; + +export type TogglePanEvent = { + type: PanAndZoomEventTypes.TOGGLE_PAN_EVT; +}; + +export type EnableTaskDraggingEvent = { + type: PanAndZoomEventTypes.DRAG_TASK_BEGIN; +}; + +export type DisableTaskDraggingEvent = { + type: PanAndZoomEventTypes.DRAG_TASK_END; +}; + +export type ToggleSearchEvent = { + type: PanAndZoomEventTypes.TOGGLE_SEARCH_EVT; +}; + +export type SelectSearchResultEvent = { + type: PanAndZoomEventTypes.SELECT_SEARCH_RESULT; + viewportOffsetWidth: number; + viewportOffsetHeight: number; +}; + +export type SetNotifiedEventTypeEvent = { + type: PanAndZoomEventTypes.SET_NOTIFIED_EVENT_TYPE; + eventType: string; +}; +export type ToggleShowDescriptionEvent = { + type: PanAndZoomEventTypes.TOGGLE_SHOW_DESCRIPTION_EVT; +}; +export type PanAndZoomEvents = + | ResetZoomPositionEvent + | SetLayoutEvent + | SetFullScreenEvent + | SetFitScreenEvent + | SelectNodeEvent + | SetInitialViewportOffsetEvent + | CenterOnSelectedTaskEvent + | HandleZoomEvent + | SetZoomEvent + | TogglePanEvent + | SetZoomToPositionEvent + | SetPositionEvent + | DragEvent + | EnableTaskDraggingEvent + | DisableTaskDraggingEvent + | ToggleSearchEvent + | SelectSearchResultEvent + | SetNotifiedEventTypeEvent + | ToggleShowDescriptionEvent; diff --git a/ui-next/src/components/flow/components/graphs/index.ts b/ui-next/src/components/flow/components/graphs/index.ts new file mode 100644 index 0000000000..0cae0f576b --- /dev/null +++ b/ui-next/src/components/flow/components/graphs/index.ts @@ -0,0 +1,5 @@ +export * from "./CustomEdgeButton"; +export * from "./CustomLabel"; +export * from "./CustomNode"; +export * from "./CustomPort"; +export * from "./PanAndZoomWrapper/ZoomControls"; diff --git a/ui-next/src/components/flow/components/shapes/DecisionOperator.tsx b/ui-next/src/components/flow/components/shapes/DecisionOperator.tsx new file mode 100644 index 0000000000..0b01c393eb --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/DecisionOperator.tsx @@ -0,0 +1,101 @@ +import StarShape from "./StarShape"; + +import { Diamond } from "@phosphor-icons/react"; +import { NodeTaskData } from "components/flow/nodes/mapper"; +import { SwitchTaskDef } from "types/TaskType"; +import { getCardVariant } from "./styles"; +import CardAttemptsBadge from "./TaskCard/CardAttemptsBadge"; +import DeleteButton from "./TaskCard/DeleteButton"; +import { showIterationChip } from "./TaskCard/helpers"; +import SwitchAdd from "./TaskCard/SwitchAdd"; +import { TaskDescription } from "./TaskDescription"; +interface DecisionOperatorProps { + nodeData: NodeTaskData; + nodeWidth: number; + portsVisible: boolean; + isInconsistent: boolean; + displayDescription?: boolean; +} + +const DecisionOperator = ({ + nodeData, + nodeWidth, + portsVisible, + isInconsistent, + displayDescription, +}: DecisionOperatorProps) => { + const { + task: { name, taskReferenceName }, + } = nodeData; + const showIterationsNumber = showIterationChip(nodeData); + return ( +
    +
    +
    + {/* Definition */} + + {showIterationsNumber ? ( + + ) : null} +
    + +
    +
    +
    + +
    +
    {name}
    +
    {taskReferenceName}
    +
    + +
    + {displayDescription && nodeData.task.description != null && ( + + )} +
    +
    + ); +}; + +export default DecisionOperator; diff --git a/ui-next/src/components/flow/components/shapes/DoWhileTask.jsx b/ui-next/src/components/flow/components/shapes/DoWhileTask.jsx new file mode 100644 index 0000000000..c20b57d1c0 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/DoWhileTask.jsx @@ -0,0 +1,185 @@ +import { IconButton, keyframes, styled } from "@mui/material"; +import { Plus, Repeat } from "@phosphor-icons/react"; +import classnames from "classnames"; +import { useDroppableNode } from "components/flow/dragDrop/hooks"; +import _isEmpty from "lodash/isEmpty"; +import { ADD_TASK_IN_DO_WHILE } from "pages/definition/state/taskModifier/constants"; +import { useMemo } from "react"; +import CardAttemptsBadge from "./TaskCard/CardAttemptsBadge"; +import CardLabel from "./TaskCard/CardLabel"; +import CardStatusBadge from "./TaskCard/CardStatusBadge"; +import DeleteButton from "./TaskCard/DeleteButton"; +import { getCardVariant } from "./styles"; + +const changeColor = keyframes` +0% { + background-position: left top, right bottom, left bottom, right top; +} +100% { + background-color: rgba(159,220,170,0.5); + background-position: left 15px top, right 15px bottom , left bottom 15px , right top 15px; +} +`; + +const DroppablePlace = styled("div")` + &.over { + background-image: + linear-gradient(90deg, silver 50%, transparent 50%), + linear-gradient(90deg, silver 50%, transparent 50%), + linear-gradient(0deg, silver 50%, transparent 50%), + linear-gradient(0deg, silver 50%, transparent 50%); + background-repeat: repeat-x, repeat-x, repeat-y, repeat-y; + background-size: + 15px 2px, + 15px 2px, + 2px 15px, + 2px 15px; + background-position: + left top, + right bottom, + left bottom, + right top; + animation: ${changeColor} 1s infinite linear; + height: 340px; + } + + &.dragging { + } + position: absolute; + top: 60px; + height: 340px; + width: ${(props) => + props.dropIsDisabled || props.draggedNodeData == null ? 0 : "350"}px; +`; + +const DoWhileTask = ({ + nodeData, + onToggleTaskMenu, + isInconsistent, + nodeId = "", + displayDescription = false, +}) => { + const { task } = nodeData; + const { type } = task; + const { + droppableResult: { isOver, setNodeRef }, + draggedNodeData, + dropIsDisabled, + } = useDroppableNode({ + nodeData: nodeData, + position: "ADD_TASK_IN_DO_WHILE", + nodeId: nodeId + "_drag_to_dowhile", + }); + + const maybeAddButton = useMemo( + () => + task.executionData == null && _isEmpty(task.loopOver) ? ( + <> + + { + onToggleTaskMenu(event, { + id: `${task.taskReferenceName}_inner_do_while`, + port: undefined, + node: { + data: { ...nodeData, action: ADD_TASK_IN_DO_WHILE }, + }, + }); + }} + style={{ + backgroundColor: "#ffffff", + }} + > + + + + ) : null, + [ + task, + nodeData, + onToggleTaskMenu, + setNodeRef, + draggedNodeData, + dropIsDisabled, + isOver, + ], + ); + + return ( +
    + {/* Execution */} + + {nodeData?.attempts > 1 ? ( + + ) : null} + + {/* Definition */} + + +
    +
    + +
    +
    + {displayDescription && nodeData.task.description != null + ? nodeData.task.description + : nodeData.task.name} +
    +
    +
    + +
    + {maybeAddButton} +
    + ); +}; + +export default DoWhileTask; diff --git a/ui-next/src/components/flow/components/shapes/DynamicTasksCards.jsx b/ui-next/src/components/flow/components/shapes/DynamicTasksCards.jsx new file mode 100644 index 0000000000..0d7710c291 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/DynamicTasksCards.jsx @@ -0,0 +1,188 @@ +import { useContext, useState } from "react"; +import CardLabel from "./TaskCard/CardLabel"; +import CardStatusBadge from "./TaskCard/CardStatusBadge"; +// import CardAttemptsBadge from "./TaskCard/CardAttemptsBadge"; +import Button from "components/MuiButton"; +import { FlowExecutionContext } from "pages/execution/state"; +import { TaskStatus } from "types/TaskStatus"; +import DeleteButton from "./TaskCard/DeleteButton"; +import { getCardVariant } from "./styles"; + +const DynamicTaskChildPlaceholder = ({ + type, + nodeData, + x, + y, + cardHeight, + ellipsis, +}) => { + const placeholderStyles = { + cursor: "pointer", + display: "flex", + width: "100%", + padding: "20px", + borderRadius: "10px", + position: "absolute", + height: `${cardHeight}px`, + transform: `translateX(${x}px) translateY(${y}px)`, + transition: "transform 0.2s ease-in-out", + ...getCardVariant(type, nodeData.status, nodeData.selected), + outlineStyle: ellipsis ? "dashed" : "solid", + }; + + if (ellipsis) { + placeholderStyles.outlineColor = "#444444"; + placeholderStyles.backgroundColor = "#FFEEAA"; + } + + if (nodeData.status === TaskStatus.PENDING) { + placeholderStyles.outlineColor = "none"; + placeholderStyles.outlineStyle = "none"; + } + + return
    ; +}; + +const DynamicTasksCards = ({ + nodeData, + isInconsistent, + displayDescription = false, +}) => { + const [isHovering, setIsHovering] = useState(false); + const { onExpandDynamic } = useContext(FlowExecutionContext); + const { task } = nodeData; + const { type } = task; + + const collapsedTasksCount = task.executionData.collapsedTasks.length; + const hoverMultiplier = 1.8; + const showEllipsisCard = collapsedTasksCount > 4; + const finalChildNumber = showEllipsisCard ? 4 : collapsedTasksCount; + + const offsetDistance = 40 / finalChildNumber; + const cardHeight = 140 - (finalChildNumber - 1) * (40 / finalChildNumber); + const initialXOffset = -(((finalChildNumber - 1) * offsetDistance) / 2); + + const completedTasks = nodeData?.collapsedTasksStatus + ? nodeData?.collapsedTasksStatus.filter((item) => item === "COMPLETED") + : []; + return ( +
    setIsHovering(true)} + onMouseLeave={() => setIsHovering(false)} + > + {[...Array(finalChildNumber)].map((_, i) => { + let xTransform = + initialXOffset + (finalChildNumber - i - 1) * offsetDistance; + const yTransform = (finalChildNumber - i - 1) * offsetDistance; + if (isHovering) { + xTransform *= hoverMultiplier; + } + + return ( + + ); + })} +
    + {/* Execution */} + + + {/* Definition */} + + +
    +
    + {displayDescription && nodeData.task.description != null ? ( + <>{nodeData.task.description} + ) : ( + <> +
    + {nodeData.task.name} +
    +
    + {nodeData.task.taskReferenceName} +
    + + )} +
    +
    + +
    + {completedTasks?.length} out of {collapsedTasksCount} task + {collapsedTasksCount > 1 ? "s" : ""} executed. +
    +
    +
    + + + {/* */} +
    +
    + ); +}; + +export default DynamicTasksCards; diff --git a/ui-next/src/components/flow/components/shapes/StarShape.jsx b/ui-next/src/components/flow/components/shapes/StarShape.jsx new file mode 100644 index 0000000000..b687c3f8cd --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/StarShape.jsx @@ -0,0 +1,50 @@ +function StarShape() { + return ( + + + + + + + + + + + + + + + + + ); +} + +export default StarShape; diff --git a/ui-next/src/components/flow/components/shapes/SubWorkflowTask.jsx b/ui-next/src/components/flow/components/shapes/SubWorkflowTask.jsx new file mode 100644 index 0000000000..50ebbb8e82 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/SubWorkflowTask.jsx @@ -0,0 +1,115 @@ +import { getCardVariant } from "./styles"; +import CardAttemptsBadge from "./TaskCard/CardAttemptsBadge"; +import CardIcon from "./TaskCard/CardIcon"; +import CardLabel from "./TaskCard/CardLabel"; +import CardStatusBadge from "./TaskCard/CardStatusBadge"; +import DeleteButton from "./TaskCard/DeleteButton"; +import { showIterationChip } from "./TaskCard/helpers"; + +const SubWorkflowTask = ({ + nodeData, + isInconsistent, + displayDescription = false, +}) => { + const { task } = nodeData; + const { type } = task; + + const subWorkflowName = task.name ? task.name : task.subWorkflowParam?.name; + const showIterationsNumber = showIterationChip(nodeData); + + return ( +
    + {/* Execution */} + + {showIterationsNumber ? ( + + ) : null} + + {/* Definition */} + + + {displayDescription && nodeData.task.description != null ? ( + <>{nodeData.task.description} + ) : ( +
    +
    + +
    + {subWorkflowName} +
    +
    +
    +
    + {task.taskReferenceName} +
    +
    +
    + )} +
    + +
    +
    + ); +}; + +export default SubWorkflowTask; diff --git a/ui-next/src/components/flow/components/shapes/SwitchJoinPseudoTask.jsx b/ui-next/src/components/flow/components/shapes/SwitchJoinPseudoTask.jsx new file mode 100644 index 0000000000..1f8cb7baf2 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/SwitchJoinPseudoTask.jsx @@ -0,0 +1,50 @@ +import { taskToSize } from "components/flow/nodes/mapper/layout"; +import { gray13, lightShadesGray } from "theme/tokens/colors"; + +const SwitchJoin = ({ nodeData }) => { + const { task } = nodeData; + const terminalClick = (event) => { + event.stopPropagation(); + }; + + const { width, height } = taskToSize(task); + return ( +
    + {`// Marks end of switch`} +
    + {task?.taskReferenceName} +
    +
    + ); +}; + +export default SwitchJoin; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/AddPathButton.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/AddPathButton.tsx new file mode 100644 index 0000000000..7512b350cc --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/AddPathButton.tsx @@ -0,0 +1,38 @@ +import Button from "components/MuiButton"; +import { WorkflowEditContext } from "pages/definition/state"; +import { + TaskAndCrumbs, + usePerformOperationOnDefinition, +} from "pages/definition/state/usePerformOperationOnDefintion"; +import { MouseEvent, ReactNode, useContext } from "react"; +import ForkIcon from "./icons/ForkIcon"; + +interface AddPathButtonProps { + children: ReactNode; + nodeData: TaskAndCrumbs; +} + +const AddPathButton = ({ children, nodeData }: AddPathButtonProps) => { + const { workflowDefinitionActor } = useContext(WorkflowEditContext); + const { handleAddSwitchPath: onAddSwitchPath } = + usePerformOperationOnDefinition(workflowDefinitionActor!); + + const handleAddEdge = (e: MouseEvent) => { + e.stopPropagation(); + onAddSwitchPath(nodeData); + }; + + return ( + + ); +}; + +export default AddPathButton; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/CardAttemptsBadge.jsx b/ui-next/src/components/flow/components/shapes/TaskCard/CardAttemptsBadge.jsx new file mode 100644 index 0000000000..a2825efa0e --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/CardAttemptsBadge.jsx @@ -0,0 +1,25 @@ +const CardAttemptsBadge = ({ attempts }) => { + return ( +
    + {attempts} +
    + ); +}; + +export default CardAttemptsBadge; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/CardIcon.jsx b/ui-next/src/components/flow/components/shapes/TaskCard/CardIcon.jsx new file mode 100644 index 0000000000..dec009a049 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/CardIcon.jsx @@ -0,0 +1,125 @@ +import { + Cards, + CloudArrowDown, + Function, + Hourglass, + Person as HumanTaskIcon, + Pause, + Repeat, + RocketLaunch, + X, + Diamond, + GitFork, + ShieldCheck, + Globe, + FileJsIcon, + HandshakeIcon, + ClockClockwiseIcon, + PersonSimpleRunIcon, + BroadcastIcon, + RowsIcon, + FilesIcon, + FileMagnifyingGlass, +} from "@phosphor-icons/react"; + +import { TaskType } from "types"; +import { ForkJoinIcon } from "./icons/ForkJoinIcon"; +import SendgridIcon from "./icons/Sendgrid"; +import HttpPollIcon from "./icons/HttpPoll"; +import JsonIcon from "./icons/Json"; +import WorkerSimpleIcon from "./icons/Worker"; +import SimpleWorkerIcon from "./icons/Simple"; +import LlmTextComplete from "./icons/LlmTextComplete"; +import LlmGenerateEmbeddings from "./icons/LlmGenerateEmbeddings"; +import LlmGetEmbeddings from "./icons/LlmGetEmbeddings"; +import LlmStoreEmbeddings from "./icons/LlmStoreEmbeddings"; +import LlmSearchIndex from "./icons/LlmSearchIndex"; +import LlmIndexDocument from "./icons/LlmIndexDocument"; +import GetDocument from "./icons/GetDocument"; +import LlmIndexText from "./icons/LlmIndexText"; +import QueryProcessor from "./icons/QueryProcessor"; +import OpsGenie from "./icons/OpsGenie"; +import UpdateTaskIcon from "./icons/UpdateTaskIcon"; +import UpdateSecretIcon from "./icons/UpdateSecret"; +import LlmChatComplete from "./icons/LlmChatComplete"; +import { IntegrationIcon } from "components/IntegrationIcon"; +import { useMemo } from "react"; + +const CardIcon = ({ type, integrationType }) => { + const MCPIntegrationIcon = useMemo(() => { + return ( +
    + +
    + ); + }, [integrationType]); + const iconMap = { + [TaskType.WAIT]: Hourglass, + [TaskType.HTTP]: Globe, + [TaskType.KAFKA_PUBLISH]: WorkerSimpleIcon, + [TaskType.HUMAN]: HumanTaskIcon, + [TaskType.BUSINESS_RULE]: HandshakeIcon, + [TaskType.SENDGRID]: SendgridIcon, + [TaskType.WAIT_FOR_WEBHOOK]: ClockClockwiseIcon, + [TaskType.HTTP_POLL]: HttpPollIcon, + [TaskType.DO_WHILE]: Repeat, + [TaskType.SIMPLE]: SimpleWorkerIcon, + [TaskType.YIELD]: Pause, + [TaskType.JDBC]: PersonSimpleRunIcon, + [TaskType.EVENT]: BroadcastIcon, + [TaskType.JOIN]: GitFork, + [TaskType.FORK_JOIN]: ForkJoinIcon, + [TaskType.FORK_JOIN_DYNAMIC]: ForkJoinIcon, + [TaskType.DYNAMIC]: Cards, + [TaskType.INLINE]: FileJsIcon, + [TaskType.SWITCH]: Diamond, + [TaskType.JSON_JQ_TRANSFORM]: JsonIcon, + [TaskType.TERMINATE]: X, + [TaskType.SET_VARIABLE]: Function, + [TaskType.TERMINATE_WORKFLOW]: X, + [TaskType.SUB_WORKFLOW]: ForkJoinIcon, + [TaskType.START_WORKFLOW]: RocketLaunch, + [TaskType.LLM_TEXT_COMPLETE]: LlmTextComplete, + [TaskType.LLM_GENERATE_EMBEDDINGS]: LlmGenerateEmbeddings, + [TaskType.LLM_GET_EMBEDDINGS]: LlmGetEmbeddings, + [TaskType.LLM_STORE_EMBEDDINGS]: LlmStoreEmbeddings, + [TaskType.LLM_INDEX_DOCUMENT]: LlmIndexDocument, + [TaskType.LLM_SEARCH_INDEX]: LlmSearchIndex, + [TaskType.LLM_INDEX_TEXT]: LlmIndexText, + [TaskType.UPDATE_SECRET]: UpdateSecretIcon, + [TaskType.GET_DOCUMENT]: GetDocument, + [TaskType.QUERY_PROCESSOR]: QueryProcessor, + [TaskType.OPS_GENIE]: OpsGenie, + [TaskType.GET_SIGNED_JWT]: ShieldCheck, + [TaskType.UPDATE_TASK]: UpdateTaskIcon, + [TaskType.GET_WORKFLOW]: CloudArrowDown, + [TaskType.LLM_CHAT_COMPLETE]: LlmChatComplete, + [TaskType.GRPC]: Globe, + [TaskType.CHUNK_TEXT]: RowsIcon, + [TaskType.LIST_FILES]: FilesIcon, + [TaskType.PARSE_DOCUMENT]: FileMagnifyingGlass, + }; + + const IconComponent = iconMap[type]; + if (type === TaskType.MCP) { + return MCPIntegrationIcon; + } + + return IconComponent ? ( +
    + +
    + ) : null; +}; + +export default CardIcon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/CardLabel.jsx b/ui-next/src/components/flow/components/shapes/TaskCard/CardLabel.jsx new file mode 100644 index 0000000000..383ce3294b --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/CardLabel.jsx @@ -0,0 +1,46 @@ +import { TaskType } from "types"; +import theme from "../../../theme"; + +const shortenedTypeTag = { + FORK_JOIN_COLLAPSED: "DYN. CHILDREN", + [TaskType.JSON_JQ_TRANSFORM]: "JSON JQ", + [TaskType.EXCLUSIVE_JOIN]: "EX. JOIN", + [TaskType.FORK_JOIN]: "FORK JOIN", + [TaskType.FORK_JOIN_DYNAMIC]: "DYN. FORK", + [TaskType.INLINE]: "INLINE", + [TaskType.KAFKA_PUBLISH]: "KAFKA", + [TaskType.SIMPLE]: "SIMPLE", +}; + +const CardLabel = ({ + type, + displayDescription = false, + integrationIconName, +}) => ( +
    +
    + {type !== TaskType.MCP + ? shortenedTypeTag[type] || type + : integrationIconName?.toUpperCase() || "MCP"} +
    +
    +); +export default CardLabel; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/CardStatusBadge.jsx b/ui-next/src/components/flow/components/shapes/TaskCard/CardStatusBadge.jsx new file mode 100644 index 0000000000..756d508a8b --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/CardStatusBadge.jsx @@ -0,0 +1,90 @@ +import { CircularProgress } from "@mui/material"; +import { + Check as CompletedIcon, + Prohibit as FailedIcon, + ArrowArcRight as SkippedTaskIcon, +} from "@phosphor-icons/react"; +import { colors } from "theme/tokens/variables"; + +import { TaskStatus } from "types/TaskStatus"; + +const getBackgroundByStatus = (status) => { + switch (status) { + case TaskStatus.COMPLETED: + return colors.primaryGreen; + case TaskStatus.COMPLETED_WITH_ERRORS: + return "#EEAA00"; + case TaskStatus.CANCELED: + return "#fba404"; + case TaskStatus.FAILED: + case TaskStatus.FAILED_WITH_TERMINAL_ERROR: + case TaskStatus.TIMED_OUT: + return "#DD2222"; + case TaskStatus.IN_PROGRESS: + case TaskStatus.SCHEDULED: + return "white"; + case TaskStatus.SKIPPED: + return "#F5BF42"; + default: + return null; + } +}; + +const CardStatusBadge = ({ status }) => { + return [ + TaskStatus.IN_PROGRESS, + TaskStatus.SCHEDULED, + TaskStatus.COMPLETED, + TaskStatus.COMPLETED_WITH_ERRORS, + TaskStatus.FAILED, + TaskStatus.FAILED_WITH_TERMINAL_ERROR, + TaskStatus.CANCELED, + TaskStatus.SKIPPED, + TaskStatus.TIMED_OUT, + ].includes(status) ? ( +
    + {[TaskStatus.IN_PROGRESS, TaskStatus.SCHEDULED].includes(status) ? ( + // disableShrink for lower CPU load + // see: https://mui.com/components/progress/ + + ) : null} + {status === TaskStatus.COMPLETED ? ( + + ) : null} + {status === TaskStatus.COMPLETED_WITH_ERRORS || + status === TaskStatus.SKIPPED ? ( + + ) : null} + {[ + TaskStatus.CANCELED, + TaskStatus.FAILED, + TaskStatus.FAILED_WITH_TERMINAL_ERROR, + TaskStatus.TIMED_OUT, + ].includes(status) ? ( + + ) : null} +
    + ) : null; +}; + +export default CardStatusBadge; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/DeleteButton.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/DeleteButton.tsx new file mode 100644 index 0000000000..7464baccaa --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/DeleteButton.tsx @@ -0,0 +1,48 @@ +import { NodeTaskData } from "components/flow/nodes/mapper"; +import { getFlowTheme } from "components/flow/theme"; +import { useContext } from "react"; +import { ColorModeContext } from "theme/material/ColorModeContext"; +import { shouldHide } from "./helpers"; +import DeleteIcon from "./icons/DeleteIcon"; + +const DeleteButton = ( + { maybeHideData }: { maybeHideData: Partial } = { + maybeHideData: { status: undefined, withinExpandedSubWorkflow: false }, + }, +) => { + const { mode } = useContext(ColorModeContext); + const theme = getFlowTheme(mode); + + return shouldHide(maybeHideData) ? ( +
    +
    + +
    +
    + ) : null; +}; + +export default DeleteButton; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/DynamicTask.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/DynamicTask.tsx new file mode 100644 index 0000000000..8d37c6bcbc --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/DynamicTask.tsx @@ -0,0 +1,36 @@ +import { NodeTaskData } from "components/flow/nodes/mapper"; +import { Link as LinkIcon } from "@phosphor-icons/react"; +import { Box, Link } from "@mui/material"; +import { cyan } from "theme/tokens/colors"; +import { DynamicTaskDef } from "types/TaskType"; + +export const DynamicTask = ({ + nodeData, +}: { + nodeData: NodeTaskData; +}) => { + const isDynamicSubWorkflow = + nodeData.task.inputParameters.taskToExecute === "SUB_WORKFLOW"; + const subWorkflowId = nodeData.outputData?.subWorkflowId as string; + + return isDynamicSubWorkflow && subWorkflowId ? ( + + + + {subWorkflowId} + + + ) : null; +}; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/EventTask.jsx b/ui-next/src/components/flow/components/shapes/TaskCard/EventTask.jsx new file mode 100644 index 0000000000..9d95234dbe --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/EventTask.jsx @@ -0,0 +1,49 @@ +import { useContext } from "react"; +import { ColorModeContext } from "theme/material/ColorModeContext"; +import { colors } from "theme/tokens/variables"; + +const EventTask = ({ nodeData }) => { + const { mode } = useContext(ColorModeContext); + const darkMode = mode === "dark"; + + const { task } = nodeData; + const { sink } = task; + + const prefix = sink?.split(":")[0]; + const value = sink?.split(":")[1]; + + return ( +
    +
    + {prefix ? ( +
    + {prefix} +
    + ) : null} +
    + {value ? value : "No Value"} +
    +
    +
    + ); +}; + +export default EventTask; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/ForkJoinDynamicTask.jsx b/ui-next/src/components/flow/components/shapes/TaskCard/ForkJoinDynamicTask.jsx new file mode 100644 index 0000000000..5b222888d6 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/ForkJoinDynamicTask.jsx @@ -0,0 +1,61 @@ +import { useSelector } from "@xstate/react"; +import { usePanAndZoomActor } from "components/flow/components/graphs/PanAndZoomWrapper"; +import { FlowActorContext } from "components/flow/state/FlowActorContext"; +import Button from "components/MuiButton"; +import { + ExecutionActionTypes, + FlowExecutionContext, +} from "pages/execution/state"; +import { useContext } from "react"; + +const ForkJoinDynamicTask = ({ nodeData }) => { + const { onCollapseDynamic } = useContext(FlowExecutionContext); + const { flowActor } = useContext(FlowActorContext); + const panAndZoomActor = useSelector( + flowActor, + (state) => state.children?.panAndZoomMachine, + ); + const [, { handleSetEventType }] = usePanAndZoomActor(panAndZoomActor); + const { collapsed, task } = nodeData; + + return ( +
    +
    +
    + {collapsed === false && task.executionData?.executed ? ( + + ) : null} +
    +
    +
    + ); +}; + +export default ForkJoinDynamicTask; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/HTTPPollTask.jsx b/ui-next/src/components/flow/components/shapes/TaskCard/HTTPPollTask.jsx new file mode 100644 index 0000000000..3f6ad95093 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/HTTPPollTask.jsx @@ -0,0 +1,57 @@ +import { Link } from "@mui/material"; +import { Link as LinkIcon } from "@phosphor-icons/react"; +import { useContext } from "react"; +import { ColorModeContext } from "theme/material/ColorModeContext"; +import { colors } from "theme/tokens/variables"; +import { isValidUri } from "./helpers"; + +const HTTPPollTask = ({ nodeData }) => { + const { mode } = useContext(ColorModeContext); + const darkMode = mode === "dark"; + + const { task } = nodeData; + const { + inputParameters: { http_request: request }, + } = task; + const isClickableUri = request?.method === "GET" && isValidUri(request?.uri); + + return ( +
    +
    + +
    + {request?.method} +
    +
    + {isClickableUri ? ( + + {request?.uri} + + ) : ( + request?.uri + )} +
    +
    +
    + ); +}; + +export default HTTPPollTask; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/HTTPTask.jsx b/ui-next/src/components/flow/components/shapes/TaskCard/HTTPTask.jsx new file mode 100644 index 0000000000..d2079ea1b3 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/HTTPTask.jsx @@ -0,0 +1,64 @@ +import { Link } from "@mui/material"; +import { Link as LinkIcon } from "@phosphor-icons/react"; +import { useContext } from "react"; +import { ColorModeContext } from "theme/material/ColorModeContext"; +import { colors } from "theme/tokens/variables"; +import { isValidUri } from "./helpers"; + +const HTTPTask = ({ nodeData }) => { + const { mode } = useContext(ColorModeContext); + const darkMode = mode === "dark"; + + const { task } = nodeData; + const { + inputParameters: { http_request: request }, + } = task; + + const method = request?.method + ? request?.method + : task?.inputParameters?.method; + + const uri = request?.uri ? request?.uri : task?.inputParameters?.uri; + + const isClickableUri = method === "GET" && isValidUri(uri); + + return ( +
    +
    + +
    + {method} +
    +
    + {isClickableUri ? ( + + {uri} + + ) : ( + uri + )} +
    +
    +
    + ); +}; + +export default HTTPTask; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/INLINETask.jsx b/ui-next/src/components/flow/components/shapes/TaskCard/INLINETask.jsx new file mode 100644 index 0000000000..c1cc8bd3e6 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/INLINETask.jsx @@ -0,0 +1,42 @@ +import { useEffect } from "react"; +import Prism from "prismjs"; +import "prismjs/themes/prism-coy.css"; + +const INLINETask = ({ nodeData }) => { + const { task } = nodeData; + + useEffect(() => { + Prism.highlightAll(); + }, []); + + const { + inputParameters: { expression }, + } = task; + + return ( + code[class*="language-"]` (!) + display: "block", + margin: "10px 0 0 0", + }} + // language-js makes JQ look pretty good! + className="language-js" + > + {expression} + + ); +}; + +export default INLINETask; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/JDBCTask.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/JDBCTask.tsx new file mode 100644 index 0000000000..c2af2dbf27 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/JDBCTask.tsx @@ -0,0 +1,29 @@ +import { Chip, Box } from "@mui/material"; +import DomainIcon from "./icons/Buildings"; +import _isNil from "lodash/isNil"; + +const statusToColor = (status?: string) => { + switch (status) { + case "COMPLETED": + return "secondary"; + case "FAILED": + return "error"; + default: + return undefined; + } +}; + +export const JDBCTask = ({ nodeData }: { nodeData: Element | any }) => { + const { task } = nodeData; + + return _isNil(task?.executionData?.domain) ? null : ( + + } + /> + + ); +}; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/JSONJQTransformTask.jsx b/ui-next/src/components/flow/components/shapes/TaskCard/JSONJQTransformTask.jsx new file mode 100644 index 0000000000..0b05fd30b6 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/JSONJQTransformTask.jsx @@ -0,0 +1,43 @@ +import { useEffect } from "react"; +import Prism from "prismjs"; +import "prismjs/themes/prism-coy.css"; + +const JSONJQTransformTask = ({ nodeData }) => { + const { task } = nodeData; + + useEffect(() => { + Prism.highlightAll(); + }, []); + + const { + inputParameters: { queryExpression }, + } = task; + + return ( + code[class*="language-"]` (!) + display: "block", + margin: "10px 0 0 0", + }} + // TODO: Support other languages according to Evaluator type. + className="language-js" + > + {typeof queryExpression === "string" ? queryExpression : ""} + + ); +}; + +export default JSONJQTransformTask; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/KAFKATask.jsx b/ui-next/src/components/flow/components/shapes/TaskCard/KAFKATask.jsx new file mode 100644 index 0000000000..a23c3cb011 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/KAFKATask.jsx @@ -0,0 +1,69 @@ +import { Link as LinkIcon, Key as KeyIcon } from "@phosphor-icons/react"; + +const KAFKATask = ({ nodeData }) => { + const { task } = nodeData; + const request = task.inputParameters?.kafka_request; + const requestKey = request?.key || {}; + + return ( +
    +
    +
    + +
    + {request?.bootStrapServers} +
    +
    + {Object.entries(requestKey)?.map(([key, value], index) => + index === 0 ? ( +
    + +
    + {`${key}: ${value}`} +
    +
    + ) : null, + )} +
    +
    + ); +}; + +export default KAFKATask; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/SimpleTask.jsx b/ui-next/src/components/flow/components/shapes/TaskCard/SimpleTask.jsx new file mode 100644 index 0000000000..345e1078b0 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/SimpleTask.jsx @@ -0,0 +1,29 @@ +import { Chip, Box } from "@mui/material"; +import DomainIcon from "./icons/Buildings"; +import _isNil from "lodash/isNil"; + +const statusToColor = (status) => { + switch (status) { + case "COMPLETED": + return "secondary"; + case "FAILED": + return "error"; + default: + return undefined; + } +}; + +export const SimpleTask = ({ nodeData }) => { + const { task } = nodeData; + + return _isNil(task?.executionData?.domain) ? null : ( + + } + /> + + ); +}; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/StartWorkflowTask.jsx b/ui-next/src/components/flow/components/shapes/TaskCard/StartWorkflowTask.jsx new file mode 100644 index 0000000000..bb65e0afb0 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/StartWorkflowTask.jsx @@ -0,0 +1,56 @@ +import { Link } from "@mui/material"; +import { TreeStructure as WorkflowIcon } from "@phosphor-icons/react"; +import { useContext } from "react"; +import { ColorModeContext } from "theme/material/ColorModeContext"; +import { colors } from "theme/tokens/variables"; + +const StartWorkflowTask = ({ nodeData }) => { + const { mode } = useContext(ColorModeContext); + const darkMode = mode === "dark"; + + const { task } = nodeData; + const { + inputParameters: { startWorkflow }, + } = task; + + return ( +
    +
    + +
    + Workflow +
    +
    + + {startWorkflow?.name} + +
    +
    +
    + ); +}; + +export default StartWorkflowTask; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/SwitchAdd.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/SwitchAdd.tsx new file mode 100644 index 0000000000..9497505002 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/SwitchAdd.tsx @@ -0,0 +1,92 @@ +import { NodeTaskData } from "components/flow/nodes/mapper"; +import { getFlowTheme } from "components/flow/theme"; +import { WorkflowEditContext } from "pages/definition/state"; +import { + TaskAndCrumbs, + usePerformOperationOnDefinition, +} from "pages/definition/state/usePerformOperationOnDefintion"; +import { MouseEvent, useContext } from "react"; +import { ColorModeContext } from "theme/material/ColorModeContext"; +import { SwitchTaskDef } from "types/TaskType"; +import { shouldHide } from "./helpers"; +import PlusIcon from "./icons/PlusIcon"; + +const getPosition = (taskcount: number) => { + switch (taskcount) { + case 1: + return { + bottom: "-12px", + right: "110px", + }; + case 2: + return { + bottom: "-12px", + right: "7px", + }; + case 3: + return { + bottom: "-12px", + right: "7px", + }; + default: + return { + bottom: "15px", + right: "-10px", + }; + } +}; + +const SwitchAdd = ( + { nodeData }: { nodeData: Partial> } = { + nodeData: { status: undefined, withinExpandedSubWorkflow: false }, + }, +) => { + const { workflowDefinitionActor } = useContext(WorkflowEditContext); + const { handleAddSwitchPath: onAddSwitchPath } = + usePerformOperationOnDefinition(workflowDefinitionActor!); + + const handleAddEdge = (e: MouseEvent) => { + e.stopPropagation(); + onAddSwitchPath(nodeData as TaskAndCrumbs); + }; + const { mode } = useContext(ColorModeContext); + const theme = getFlowTheme(mode); + + return shouldHide(nodeData) ? ( +
    +
    + +
    +
    + ) : null; +}; + +export default SwitchAdd; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/TaskCard.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/TaskCard.tsx new file mode 100644 index 0000000000..ae8c975faa --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/TaskCard.tsx @@ -0,0 +1,198 @@ +import HTTPPollTask from "components/flow/components/shapes/TaskCard/HTTPPollTask"; +import { JDBCTask } from "components/flow/components/shapes/TaskCard/JDBCTask"; +import StartWorkflowTask from "components/flow/components/shapes/TaskCard/StartWorkflowTask"; +import { NodeTaskData } from "components/flow/nodes/mapper"; +import { TaskAndCrumbs } from "pages/definition/state/usePerformOperationOnDefintion"; +import { useContext } from "react"; +import { ColorModeContext } from "theme/material/ColorModeContext"; +import { colors } from "theme/tokens/variables"; +import { DynamicTaskDef, TaskStatus, TaskType, WaitTaskDef } from "types"; +import { MCPTaskDef } from "types/TaskType"; +import { getCardVariant } from "../styles"; +import AddPathButton from "./AddPathButton"; +import CardAttemptsBadge from "./CardAttemptsBadge"; +import CardIcon from "./CardIcon"; +import CardLabel from "./CardLabel"; +import CardStatusBadge from "./CardStatusBadge"; +import DeleteButton from "./DeleteButton"; +import { DynamicTask } from "./DynamicTask"; +import EventTask from "./EventTask"; +import ForkJoinDynamicTask from "./ForkJoinDynamicTask"; +import { showIterationChip } from "./helpers"; +import HTTPTask from "./HTTPTask"; +import INLINETask from "./INLINETask"; +import JSONJQTransformTask from "./JSONJQTransformTask"; +import KAFKATask from "./KAFKATask"; +import { SimpleTask } from "./SimpleTask"; +import { WaitTaskInfo } from "./WaitTaskInfo"; +import { TaskDescription } from "../TaskDescription"; + +const getTaskCardContent = (type: TaskType, nodeData: NodeTaskData) => { + switch (type) { + case TaskType.HTTP: + return ; + case TaskType.HTTP_POLL: + return ; + case TaskType.JSON_JQ_TRANSFORM: + return ; + case TaskType.INLINE: + return ; + case TaskType.KAFKA_PUBLISH: + return ; + case TaskType.FORK_JOIN_DYNAMIC: + return ; + case TaskType.EVENT: + return ; + case TaskType.SIMPLE: + return ; + case TaskType.JDBC: + return ; + case TaskType.START_WORKFLOW: + return ; + case TaskType.DYNAMIC: + return ( + } /> + ); + default: + return null; + } +}; + +const TaskCard = ({ + nodeData, + onClick = () => null, + isInconsistent, + displayDescription, +}: { + nodeData: NodeTaskData; + onClick: () => void; + isInconsistent: boolean; + displayDescription?: boolean; +}) => { + const { mode } = useContext(ColorModeContext); + const darkMode = mode === "dark"; + + const { task, status } = nodeData; + const { name, type, taskReferenceName } = task; + + const showIterationsNumber = showIterationChip(nodeData); + return ( +
    +
    + {/* Execution */} + + {showIterationsNumber ? ( + + ) : null} + + {/* Definition */} + + +
    + + +
    +
    + {name} +
    +
    + {taskReferenceName} +
    +
    + {!status && type === TaskType.FORK_JOIN ? ( + + Add fork + + ) : null} + {type === TaskType.WAIT && + ((task as WaitTaskDef)?.inputParameters?.duration || + (task as WaitTaskDef)?.inputParameters?.until) ? ( + + ) : null} +
    +
    + + +
    +
    {getTaskCardContent(type, nodeData)}
    + + {displayDescription && task.description != null && ( + + )} +
    +
    + ); +}; + +export default TaskCard; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/WaitTaskInfo.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/WaitTaskInfo.tsx new file mode 100644 index 0000000000..d9035d732e --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/WaitTaskInfo.tsx @@ -0,0 +1,66 @@ +import { Typography } from "@mui/material"; +import { Box } from "@mui/system"; +import { ClockIcon } from "@phosphor-icons/react"; +import { WaitTaskDef } from "types"; + +interface WaitTaskInfoProps { + task: WaitTaskDef; +} + +export const WaitTaskInfo = ({ task }: WaitTaskInfoProps) => { + const duration = task?.inputParameters?.duration; + const until = task?.inputParameters?.until; + + if (!duration && !until) { + return null; + } + + // Determine label and display value + const isUntil = !!until; + const label = isUntil ? "Until" : "Duration"; + + const durationDisplay = duration ? `${duration}` : until ? `${until}` : ""; + const durationDisplayLineHeight = + durationDisplay.length > 30 ? "14px" : "auto"; + + return ( + + {/* Duration/Until Section */} + + + + + + {`${label}: ${durationDisplay}`} + + + + ); +}; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/helpers.test.ts b/ui-next/src/components/flow/components/shapes/TaskCard/helpers.test.ts new file mode 100644 index 0000000000..789675decc --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/helpers.test.ts @@ -0,0 +1,168 @@ +import { dowhileHasAllIterationsInOutput, showIterationChip } from "./helpers"; + +// this test is meant to check if the outputData of dowhile is not summarized.(no data loss) +describe("dowhileHasAllIterationsInOutput", () => { + const outputData = { + "1": {}, + "2": {}, + iteration: 2, + }; + const outputDataWhileWorkflowInProgress = { + "1": {}, + "2": {}, + iteration: 3, + }; + const summarizedOutputData = { + "119": {}, + "120": {}, + "121": {}, + iteration: 121, + }; + + it("Should return true, as the output data is not summarized as it has all the output from 1 to iteration number", () => { + const result = dowhileHasAllIterationsInOutput(outputData); + expect(result).toBe(true); + }); + it("Should return false, as the output data is summarized as it doesn't have all the output from 1 to iteration number", () => { + const result = dowhileHasAllIterationsInOutput(summarizedOutputData); + expect(result).toBe(false); + }); + // since the backend sends n-1 iterations in outputData while the workflow is running, we are doing the below test. + it("Should return true, as the output data is not summarized as it doesn't have all the output from 1 to (iteration number - 1) when Workflow in progress", () => { + const result = dowhileHasAllIterationsInOutput( + outputDataWhileWorkflowInProgress, + ); + expect(result).toBe(true); + }); +}); + +describe("showIterationChip", () => { + const nodeDataWithKeepLastN = { + attempts: 20, + parentLoop: { + inputData: { + keepLastN: 10, + }, + outputData: { + "11": {}, + "12": {}, + "13": {}, + "14": {}, + "15": {}, + "16": {}, + "17": {}, + "18": {}, + "19": {}, + "20": {}, + iteration: 20, + }, + }, + }; + const nodeDataWithoutKeepLastNAndSummarized = { + attempts: 20, + parentLoop: { + inputData: {}, + outputData: { + "11": {}, + "12": {}, + "13": {}, + "14": {}, + "15": {}, + "16": {}, + "17": {}, + "18": {}, + "19": {}, + "20": {}, + iteration: 20, + }, + }, + }; + + const nodeDataWithoutKeepLastNAndNotSummarized = { + attempts: 10, + parentLoop: { + inputData: {}, + outputData: { + "1": {}, + "2": {}, + "3": {}, + "4": {}, + "5": {}, + "6": {}, + "7": {}, + "8": {}, + "9": {}, + "10": {}, + iteration: 10, + }, + }, + }; + const nodeDataWithoutKeepLastNAndNotSummarized2 = { + attempts: 10, + parentLoop: { + inputData: {}, + outputData: { + "1": {}, + "2": {}, + "3": {}, + "4": {}, + "5": {}, + "6": {}, + "7": {}, + "8": {}, + "9": {}, + iteration: 10, + }, + }, + }; + const nodeDataWithKeepLastNAndNotSummarized = { + attempts: 10, + parentLoop: { + inputData: { + keepLastN: 10, + }, + outputData: { + "1": {}, + "2": {}, + "3": {}, + "4": {}, + "5": {}, + "6": {}, + "7": {}, + "8": {}, + "9": {}, + iteration: 10, + }, + }, + }; + + it("Should return false, as the keepLastN is available - dont show iteration chip", () => { + const result = showIterationChip(nodeDataWithKeepLastN as any); + expect(result).toBe(false); + }); + it("Should return false, as eventhough the keepLastN is not available, but the output is summarized - dont show iteration chip", () => { + const result = showIterationChip( + nodeDataWithoutKeepLastNAndSummarized as any, + ); + expect(result).toBe(false); + }); + it("Should return true, as eventhough it doesn't have keepLastN, but the output is not summarized - show iteration chip", () => { + const result = showIterationChip( + nodeDataWithoutKeepLastNAndNotSummarized as any, + ); + expect(result).toBe(true); + }); + // since the backend sends n-1 iterations in outputData while the workflow is running, we are doing the below test. + it("Should return true, as eventhough it doesn't have keepLastN, and having n-1 iterations data in output.and output is not summarized - show iteration chip", () => { + const result = showIterationChip( + nodeDataWithoutKeepLastNAndNotSummarized2 as any, + ); + expect(result).toBe(true); + }); + it("Should return false, as eventhough output is not summarized it has keepLastN. - dont show iteration chip", () => { + const result = showIterationChip( + nodeDataWithKeepLastNAndNotSummarized as any, + ); + expect(result).toBe(false); + }); +}); diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/helpers.ts b/ui-next/src/components/flow/components/shapes/TaskCard/helpers.ts new file mode 100644 index 0000000000..2f6e3a3142 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/helpers.ts @@ -0,0 +1,43 @@ +import { NodeTaskData } from "components/flow/nodes/mapper"; + +export const shouldHide = ( + { + status = undefined, + withinExpandedSubWorkflow = false, + }: Partial = { + status: undefined, + withinExpandedSubWorkflow: false, + }, +) => !status && !withinExpandedSubWorkflow; + +export function dowhileHasAllIterationsInOutput( + outputData: Record, +): boolean { + const max = outputData?.iteration as number; + for (let i = 1; i < max; i++) { + if (!Object.prototype.hasOwnProperty.call(outputData, String(i))) { + return false; + } + } + return true; +} + +export function showIterationChip(nodeData: NodeTaskData): boolean { + const keepLastN = nodeData?.parentLoop?.inputData?.keepLastN; + return ( + !keepLastN && + dowhileHasAllIterationsInOutput(nodeData?.parentLoop?.outputData ?? {}) && + typeof nodeData?.attempts === "number" && + nodeData.attempts > 1 + ); +} + +// Helper function to check if a string is a valid URI +export const isValidUri = (uriString: string) => { + try { + new URL(uriString); + return true; + } catch { + return false; + } +}; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/Buildings.jsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/Buildings.jsx new file mode 100644 index 0000000000..2a15077ca8 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/Buildings.jsx @@ -0,0 +1,72 @@ +import React from "react"; + +function Icon({ size, color }) { + return ( + + + + + + + + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/BusinessRule.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/BusinessRule.tsx new file mode 100644 index 0000000000..83b79667c3 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/BusinessRule.tsx @@ -0,0 +1,64 @@ +import type { CustomIconType } from "./types"; +function Icon({ size, color = "#000000" }: CustomIconType) { + return ( + + + + + + + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/CheckIcon.jsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/CheckIcon.jsx new file mode 100644 index 0000000000..7046bd6889 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/CheckIcon.jsx @@ -0,0 +1,22 @@ +function Icon({ size, color }) { + return ( + + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/DeleteIcon.jsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/DeleteIcon.jsx new file mode 100644 index 0000000000..383e0193d2 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/DeleteIcon.jsx @@ -0,0 +1,33 @@ +// From phosphoricons +// rendering the svg directly for performance + +function DeleteIcon({ size = 24, color = "#000" }) { + return ( + + + + + + ); +} + +export default DeleteIcon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/DynamicFanout.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/DynamicFanout.tsx new file mode 100644 index 0000000000..5e68b173bf --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/DynamicFanout.tsx @@ -0,0 +1,21 @@ +import type { CustomIconType } from "./types"; +function DynamicFanoutIcon({ size, color = "#000000" }: CustomIconType) { + return ( + + + + ); +} + +export default DynamicFanoutIcon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/DynamicFork.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/DynamicFork.tsx new file mode 100644 index 0000000000..ec5933446b --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/DynamicFork.tsx @@ -0,0 +1,19 @@ +import type { CustomIconType } from "./types"; +function Icon({ size, color = "#000000" }: CustomIconType) { + return ( + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/Event.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/Event.tsx new file mode 100644 index 0000000000..60f3d38c92 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/Event.tsx @@ -0,0 +1,33 @@ +import type { CustomIconType } from "./types"; + +function Icon({ size, color = "#000000" }: CustomIconType) { + return ( + + + + + + + + + + + + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/ExclamationCircleIcon.jsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/ExclamationCircleIcon.jsx new file mode 100644 index 0000000000..ac620843f8 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/ExclamationCircleIcon.jsx @@ -0,0 +1,34 @@ +// From phosphoricons +// rendering the svg directly for performance + +function ExclamationCircleIcon({ size, color }) { + return ( + + + + + + ); +} + +export default ExclamationCircleIcon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/ForkIcon.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/ForkIcon.tsx new file mode 100644 index 0000000000..6f7086a4de --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/ForkIcon.tsx @@ -0,0 +1,25 @@ +import type { CustomIconType } from "./types"; +function ForkIcon({ + size = "24", + color = "#000", + flip = false, +}: CustomIconType) { + return ( + + + + + + + + + + ); +} + +export default ForkIcon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/ForkJoinIcon.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/ForkJoinIcon.tsx new file mode 100644 index 0000000000..fad799fa23 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/ForkJoinIcon.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { GitFork } from "@phosphor-icons/react"; + +export const ForkJoinIcon = () => + React.createElement( + "div", + { + style: { + transform: "rotate(180deg)", + display: "inline-flex", + alignItems: "center", + justifyContent: "center", + }, + }, + React.createElement(GitFork, { size: 24 }), + ); diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/GetDocument.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/GetDocument.tsx new file mode 100644 index 0000000000..f3e098066c --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/GetDocument.tsx @@ -0,0 +1,31 @@ +function Icon({ size = "24", color = "currentColor" }) { + return ( + + + + + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/GetWorkflow.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/GetWorkflow.tsx new file mode 100644 index 0000000000..ddb3e0fe15 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/GetWorkflow.tsx @@ -0,0 +1,20 @@ +import type { CustomIconType } from "./types"; + +function Icon({ size = "24", color = "#212121" }: CustomIconType) { + return ( + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/Http.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/Http.tsx new file mode 100644 index 0000000000..8f2c2e8c9e --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/Http.tsx @@ -0,0 +1,58 @@ +import type { CustomIconType } from "./types"; + +function Icon({ size = "24", color = "" }: CustomIconType) { + return ( + + + + + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/HttpPoll.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/HttpPoll.tsx new file mode 100644 index 0000000000..21f34b641a --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/HttpPoll.tsx @@ -0,0 +1,28 @@ +import type { CustomIconType } from "./types"; + +function Icon({ size = "24", color = "currentColor" }: CustomIconType) { + return ( + + + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/Inline.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/Inline.tsx new file mode 100644 index 0000000000..9eafba0614 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/Inline.tsx @@ -0,0 +1,48 @@ +import type { CustomIconType } from "./types"; +function Icon({ size = "24", color = "#000000" }: CustomIconType) { + return ( + + + + + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/Json.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/Json.tsx new file mode 100644 index 0000000000..bc0e5f4076 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/Json.tsx @@ -0,0 +1,50 @@ +import type { CustomIconType } from "./types"; +function Icon({ size, color = "currentColor" }: CustomIconType) { + return ( + + + + + + + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/Kafka.jsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/Kafka.jsx new file mode 100644 index 0000000000..24aa8e3b35 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/Kafka.jsx @@ -0,0 +1,15 @@ +function Icon({ size = "24" }) { + return ( + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/LlmChatComplete.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/LlmChatComplete.tsx new file mode 100644 index 0000000000..6867e7c0a6 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/LlmChatComplete.tsx @@ -0,0 +1,18 @@ +function Icon({ size = "24", color = "currentColor" }) { + return ( + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/LlmGenerateEmbeddings.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/LlmGenerateEmbeddings.tsx new file mode 100644 index 0000000000..0f7030cbde --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/LlmGenerateEmbeddings.tsx @@ -0,0 +1,30 @@ +function Icon({ size = "24", color = "currentColor" }) { + return ( + + + + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/LlmGetEmbeddings.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/LlmGetEmbeddings.tsx new file mode 100644 index 0000000000..3b731e14fb --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/LlmGetEmbeddings.tsx @@ -0,0 +1,30 @@ +function Icon({ size = "24", color = "currentColor" }) { + return ( + + + + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/LlmIndexDocument.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/LlmIndexDocument.tsx new file mode 100644 index 0000000000..f7a1cddae7 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/LlmIndexDocument.tsx @@ -0,0 +1,30 @@ +function Icon({ size = "24", color = "currentColor" }) { + return ( + + + + + + + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/LlmIndexText.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/LlmIndexText.tsx new file mode 100644 index 0000000000..d3ed5c72e7 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/LlmIndexText.tsx @@ -0,0 +1,45 @@ +function Icon({ size = "24", color = "currentColor" }) { + return ( + + + + + + + + + + + + + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/LlmSearchIndex.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/LlmSearchIndex.tsx new file mode 100644 index 0000000000..6b5e15f0a4 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/LlmSearchIndex.tsx @@ -0,0 +1,30 @@ +function Icon({ size = "24", color = "currentColor" }) { + return ( + + + + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/LlmStoreEmbeddings.jsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/LlmStoreEmbeddings.jsx new file mode 100644 index 0000000000..634386d796 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/LlmStoreEmbeddings.jsx @@ -0,0 +1,18 @@ +function Icon({ size = "24", color = "currentColor" }) { + return ( + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/LlmTextComplete.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/LlmTextComplete.tsx new file mode 100644 index 0000000000..a81fa4d099 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/LlmTextComplete.tsx @@ -0,0 +1,50 @@ +function Icon({ size = "24", color = "currentColor" }) { + return ( + + + + + + + + + + + + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/LoopIcon.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/LoopIcon.tsx new file mode 100644 index 0000000000..584b41a868 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/LoopIcon.tsx @@ -0,0 +1,50 @@ +import type { CustomIconType } from "./types"; +// From phosphoricons +// rendering the svg directly for performance + +function LoopIcon({ color = "#000000", size = "24" }: CustomIconType) { + return ( + + + + + + + + ); +} + +export default LoopIcon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/MCPIcon.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/MCPIcon.tsx new file mode 100644 index 0000000000..4628ee68f2 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/MCPIcon.tsx @@ -0,0 +1,20 @@ +import React from "react"; + +function MCPIcon({ size = "24" }) { + return ( + + ModelContextProtocol + + + + ); +} + +export default MCPIcon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/MergeIcon.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/MergeIcon.tsx new file mode 100644 index 0000000000..b9fb2dc1ca --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/MergeIcon.tsx @@ -0,0 +1,48 @@ +function MergeIcon({ size = "24", color = "#212121" }) { + return ( + + + + + + + + + + + + + + + + + + + + + + + ); +} +export default MergeIcon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/MinusIcon.jsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/MinusIcon.jsx new file mode 100644 index 0000000000..031e80da27 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/MinusIcon.jsx @@ -0,0 +1,22 @@ +function MinusIcon({ size = 24, color = "#000" }) { + return ( + + + + + ); +} + +export default MinusIcon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/OpsGenie.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/OpsGenie.tsx new file mode 100644 index 0000000000..2adef86cd7 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/OpsGenie.tsx @@ -0,0 +1,26 @@ +function OpsGenie({ size = "24", color = "currentColor" }) { + return ( + + + + + + ); +} + +export default OpsGenie; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/PlusIcon.jsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/PlusIcon.jsx new file mode 100644 index 0000000000..a0e9265b28 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/PlusIcon.jsx @@ -0,0 +1,30 @@ +function PlusIcon({ size = 24, color = "#000" }) { + return ( + + + + + + ); +} + +export default PlusIcon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/QueryProcessor.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/QueryProcessor.tsx new file mode 100644 index 0000000000..2a66361092 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/QueryProcessor.tsx @@ -0,0 +1,18 @@ +function QueryProcessor({ size = "24", color = "currentColor" }) { + return ( + + + + ); +} + +export default QueryProcessor; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/Sendgrid.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/Sendgrid.tsx new file mode 100644 index 0000000000..9abc4a02fc --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/Sendgrid.tsx @@ -0,0 +1,29 @@ +function Icon() { + return ( + + + + + + + + + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/Simple.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/Simple.tsx new file mode 100644 index 0000000000..bb451d5937 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/Simple.tsx @@ -0,0 +1,16 @@ +import type { CustomIconType } from "./types"; +function Icon({ size, color = "currentColor" }: CustomIconType) { + return ( + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/StackIcon.jsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/StackIcon.jsx new file mode 100644 index 0000000000..1e0260cd63 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/StackIcon.jsx @@ -0,0 +1,41 @@ +// From phosphoricons, +// rendering the svg directly for performance + +function StackIcon({ color = "#000000", size = 24 }) { + return ( + + + + + + + ); +} + +export default StackIcon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/SubWorkflow.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/SubWorkflow.tsx new file mode 100644 index 0000000000..c7970b20f9 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/SubWorkflow.tsx @@ -0,0 +1,22 @@ +import type { CustomIconType } from "./types"; +function SubWorkflowIcon({ size, color = "#000000" }: CustomIconType) { + return ( + + + + + ); +} + +export default SubWorkflowIcon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/Switch.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/Switch.tsx new file mode 100644 index 0000000000..8781714b0b --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/Switch.tsx @@ -0,0 +1,38 @@ +import type { CustomIconType } from "./types"; +function Icon({ size, color = "#000000" }: CustomIconType) { + return ( + + + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/Terminate.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/Terminate.tsx new file mode 100644 index 0000000000..e20d0d6cdc --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/Terminate.tsx @@ -0,0 +1,17 @@ +import type { CustomIconType } from "./types"; +function TerminateIcon({ size, color = "#000000" }: CustomIconType) { + return ( + + + + ); +} + +export default TerminateIcon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/TerminateWorkFlow.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/TerminateWorkFlow.tsx new file mode 100644 index 0000000000..c90dec64ad --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/TerminateWorkFlow.tsx @@ -0,0 +1,76 @@ +import type { CustomIconType } from "./types"; +function Icon({ size, color = "#000000" }: CustomIconType) { + return ( + + + + + + + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/UpdateSecret.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/UpdateSecret.tsx new file mode 100644 index 0000000000..9d5b0bed36 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/UpdateSecret.tsx @@ -0,0 +1,27 @@ +import type { CustomIconType } from "./types"; +function Icon({ size, color = "currentColor" }: CustomIconType) { + return ( + + + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/UpdateTaskIcon.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/UpdateTaskIcon.tsx new file mode 100644 index 0000000000..22b3bda3c9 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/UpdateTaskIcon.tsx @@ -0,0 +1,32 @@ +import type { CustomIconType } from "./types"; +function Icon({ size, color = "currentColor" }: CustomIconType) { + return ( + + + + + + + + + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/Variable.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/Variable.tsx new file mode 100644 index 0000000000..2aa5f63e99 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/Variable.tsx @@ -0,0 +1,21 @@ +import type { CustomIconType } from "./types"; +function Icon({ size, color = "#000000" }: CustomIconType) { + return ( + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/Wait.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/Wait.tsx new file mode 100644 index 0000000000..77520c7046 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/Wait.tsx @@ -0,0 +1,55 @@ +import type { CustomIconType } from "./types"; + +function Icon({ size, color = "#000000" }: CustomIconType) { + return ( + + + + + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/WaitForWebhook.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/WaitForWebhook.tsx new file mode 100644 index 0000000000..a90cafdc3b --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/WaitForWebhook.tsx @@ -0,0 +1,54 @@ +import type { CustomIconType } from "./types"; +function Icon({ size, color = "#000000" }: CustomIconType) { + return ( + + + + + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/WarningIcon.jsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/WarningIcon.jsx new file mode 100644 index 0000000000..571530c114 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/WarningIcon.jsx @@ -0,0 +1,31 @@ +function Icon({ size, color }) { + return ( + + + + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/WorkFlow.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/WorkFlow.tsx new file mode 100644 index 0000000000..eeaed96a9b --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/WorkFlow.tsx @@ -0,0 +1,71 @@ +import type { CustomIconType } from "./types"; +function Icon({ size, color = "#000000" }: CustomIconType) { + return ( + + + + + + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/Worker.tsx b/ui-next/src/components/flow/components/shapes/TaskCard/icons/Worker.tsx new file mode 100644 index 0000000000..c0ac9c00c1 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/Worker.tsx @@ -0,0 +1,50 @@ +import type { CustomIconType } from "./types"; +function Icon({ size, color = "#000000" }: CustomIconType) { + return ( + + + + + + + + ); +} + +export default Icon; diff --git a/ui-next/src/components/flow/components/shapes/TaskCard/icons/types.ts b/ui-next/src/components/flow/components/shapes/TaskCard/icons/types.ts new file mode 100644 index 0000000000..adeabc8c30 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskCard/icons/types.ts @@ -0,0 +1,9 @@ +import { CSSProperties } from "react"; + +export type CustomIconType = { + size?: string | number; + color?: string; + className?: string; + style?: CSSProperties; + flip?: boolean; +}; diff --git a/ui-next/src/components/flow/components/shapes/TaskDescription.tsx b/ui-next/src/components/flow/components/shapes/TaskDescription.tsx new file mode 100644 index 0000000000..bc424c8973 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskDescription.tsx @@ -0,0 +1,73 @@ +import { useRef } from "react"; +import { TaskType } from "types"; +import { Fade } from "@mui/material"; + +const OPERATOR_TASK_TYPES = [ + TaskType.FORK_JOIN_DYNAMIC, + TaskType.JOIN, + TaskType.FORK_JOIN, + TaskType.FORK_JOIN_DYNAMIC, + TaskType.TERMINATE, + TaskType.SUB_WORKFLOW, + TaskType.DYNAMIC, + TaskType.TERMINATE_WORKFLOW, + TaskType.SET_VARIABLE, + TaskType.WAIT, + TaskType.START_WORKFLOW, +]; + +export const TaskDescription = ({ + description, + taskType, +}: { + description: string; + taskType: TaskType; +}) => { + const divRef = useRef(null); + + let borderColor = "rgba(0, 0, 0, 0.1)"; + let borderTopColor = "#cccccc"; + let color = "#555555"; + let textShadow = "none"; + let background = "rgba(255, 255, 255, 0.35)"; + if (OPERATOR_TASK_TYPES.includes(taskType)) { + borderColor = "rgba(255, 255, 255, 0.2)"; + color = "white"; + textShadow = "0 0 2px rgba(0, 0, 0, 1)"; + borderTopColor = "rgba(255, 255, 255, 0.5)"; + background = "rgba(255, 255, 255, 0.3)"; + } + + return ( + +
    + {description} +
    +
    + ); +}; diff --git a/ui-next/src/components/flow/components/shapes/TaskShape/Shape.tsx b/ui-next/src/components/flow/components/shapes/TaskShape/Shape.tsx new file mode 100644 index 0000000000..5bb5e0c4f0 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskShape/Shape.tsx @@ -0,0 +1,116 @@ +import { DraggableSyntheticListeners } from "@dnd-kit/core"; +import { Handle } from "components/flow/dragDrop/Handle"; +import { BOTTOM_PORT_MARGIN, NodeTaskData } from "components/flow/nodes/mapper"; +import { CSSProperties, forwardRef, ReactNode, useMemo } from "react"; +import { CommonTaskDef, SwitchTaskDef, TaskStatus, TaskType } from "types"; +import DecisionOperator from "../DecisionOperator"; +import DoWhileTask from "../DoWhileTask"; +import DynamicTasksCards from "../DynamicTasksCards"; +import SubWorkflowTask from "../SubWorkflowTask"; +import SwitchJoin from "../SwitchJoinPseudoTask"; +import TaskCard from "../TaskCard/TaskCard"; +import TaskSummary from "../TaskSummary"; +import TerminalTask from "../TerminalTask"; + +interface ShapeProps { + displayDescription?: boolean; + type: ShapeComponentForTypeParams; + nodeData: NodeTaskData; + onToggleTaskMenu: (event: any) => void; + portsVisible?: boolean; + nodeWidth?: number; + nodeHeight?: number; + isInconsistent: boolean; + listeners?: DraggableSyntheticListeners; + style?: CSSProperties; + handle?: boolean; + nodeId?: string; +} +export type ShapeComponentForTypeParams = TaskType & "FORK_JOIN_COLLAPSED"; + +type ShapePropsToShape = ( + props: ShapeProps, +) => ReactNode; + +const DecisionOperatorShape: ShapePropsToShape = ( + props: ShapeProps, +) => ( + +); + +const SHAPES_FOR_TYPE = { + FORK_JOIN_COLLAPSED: (props: ShapeProps) => , + [TaskType.DO_WHILE]: (props: ShapeProps) => , + [TaskType.TERMINAL]: (props: ShapeProps) => ( + + ), + [TaskType.SWITCH]: DecisionOperatorShape, + [TaskType.DECISION]: DecisionOperatorShape, + [TaskType.TASK_SUMMARY]: (props: ShapeProps) => , + [TaskType.SUB_WORKFLOW]: (props: ShapeProps) => ( + + ), + [TaskType.SWITCH_JOIN]: (props: ShapeProps) => , +} satisfies Record; + +export const Shape = forwardRef((props, ref) => { + const { + type, + nodeData, + portsVisible, + nodeWidth, + nodeHeight, + listeners, + style = {}, + handle = true, + } = props; + const dimTask = [TaskStatus.PENDING, TaskStatus.SKIPPED].includes( + nodeData.status!, + ); + const containerStyles = useMemo(() => { + const extraHeight = type === "FORK_JOIN_DYNAMIC" ? 10 : 0; + const bottomMargin = portsVisible ? BOTTOM_PORT_MARGIN : 0; + + return { + display: "flex", + top: 0, + left: "200px", + justifyContent: "center", + opacity: !dimTask ? 1 : 0.75, + filter: !dimTask ? "" : "grayscale(.75)", + padding: "0", + width: nodeWidth || 0, + height: (nodeHeight || 0) - bottomMargin + extraHeight, + ...style, + }; + }, [dimTask, nodeWidth, nodeHeight, type, portsVisible, style]); + + const ShapeComponent: ShapePropsToShape = useMemo( + () => SHAPES_FOR_TYPE[type] ?? TaskCard, + [type], + ); + + const handleStyles = useMemo(() => { + // The Switch Task relies on having extra 100 pixels to space the ports. this positions the handle for that task. + return [TaskType.DECISION, TaskType.SWITCH].includes(type) + ? { left: "50px" } + : {}; + }, [type]); + + return ( +
    + {handle ? : null} + +
    + ); +}); diff --git a/ui-next/src/components/flow/components/shapes/TaskShape/TaskShape.tsx b/ui-next/src/components/flow/components/shapes/TaskShape/TaskShape.tsx new file mode 100644 index 0000000000..5764cb3579 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskShape/TaskShape.tsx @@ -0,0 +1,57 @@ +import { FunctionComponent, ReactNode } from "react"; +import { NodeTaskData } from "components/flow/nodes/mapper"; +import { Shape, ShapeComponentForTypeParams } from "./Shape"; +import { useDraggableNode } from "components/flow/dragDrop"; + +interface TaskShapeProps { + onToggleTaskMenu: (event: any) => void; + nodeData: NodeTaskData & { selected?: boolean }; + isInconsistent: boolean; + width?: number; + height?: number; + portsVisible?: boolean; + children?: ReactNode; + nodeId: string; + displayDescription?: boolean; +} + +export const TaskShape: FunctionComponent = ({ + onToggleTaskMenu, + nodeData, + width = undefined, + height = undefined, + portsVisible = false, + isInconsistent, + nodeId, + displayDescription, +}) => { + const { task } = nodeData; + const { type } = task; + + const { + draggableResult: { listeners, setNodeRef }, + dragIsDisabled, + } = useDraggableNode({ + nodeData, + width, + height, + nodeId, + }); + + return ( + + ); +}; diff --git a/ui-next/src/components/flow/components/shapes/TaskShape/index.ts b/ui-next/src/components/flow/components/shapes/TaskShape/index.ts new file mode 100644 index 0000000000..98cfe42d02 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskShape/index.ts @@ -0,0 +1,2 @@ +export * from "./TaskShape"; +export * from "./Shape"; diff --git a/ui-next/src/components/flow/components/shapes/TaskSummary.jsx b/ui-next/src/components/flow/components/shapes/TaskSummary.jsx new file mode 100644 index 0000000000..bf6b4db6ec --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TaskSummary.jsx @@ -0,0 +1,114 @@ +import StatusBadge from "components/StatusBadge"; +import { taskStatusCompareFn } from "utils"; +import CardLabel from "./TaskCard/CardLabel"; +import CardStatusBadge from "./TaskCard/CardStatusBadge"; +import { getCardVariant } from "./styles"; + +const TaskSummary = (props) => { + const { nodeData, nodeHeight } = props; + const { task } = nodeData; + const { type } = task; + + return ( +
    +
    + {/* Execution */} + + + {/* Definition */} + +
    +
    +
    + {nodeData.task.name} +
    +
    + {nodeData.task.taskReferenceName} +
    +
    +
    +
    + {Object.entries(nodeData?.summary?.taskCountByStatus) + .sort(([key1], [key2]) => taskStatusCompareFn(key1, key2)) + .map(([key, value]) => ( +
    + + + {value} + +
    + ))} +
    +
    +
    + + +
    +
    + ); +}; + +export default TaskSummary; diff --git a/ui-next/src/components/flow/components/shapes/TerminalTask.jsx b/ui-next/src/components/flow/components/shapes/TerminalTask.jsx new file mode 100644 index 0000000000..1cf83a5072 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/TerminalTask.jsx @@ -0,0 +1,50 @@ +import { + BOTTOM_PORT_MARGIN, + taskToSize, +} from "components/flow/nodes/mapper/layout"; +import { getFlowTheme } from "components/flow/theme"; +import { useContext } from "react"; +import { ColorModeContext } from "theme/material/ColorModeContext"; + +const TerminalTask = ({ nodeData, portsVisible }) => { + const { mode } = useContext(ColorModeContext); + const theme = getFlowTheme(mode); + + const { task } = nodeData; + const terminalClick = (event) => { + event.stopPropagation(); + }; + + const { width, height } = taskToSize(task); + return ( +
    +
    + {task.name === "start" ? "Start" : "End"} +
    +
    + ); +}; + +export default TerminalTask; diff --git a/ui-next/src/components/flow/components/shapes/styles.ts b/ui-next/src/components/flow/components/shapes/styles.ts new file mode 100644 index 0000000000..7aa1ddb66f --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/styles.ts @@ -0,0 +1,91 @@ +import theme from "components/flow/theme"; +import { TaskStatus, TaskType } from "types"; + +export const getCardVariant = ( + type: TaskType, + status?: TaskStatus, + selected?: boolean, +) => { + const outlineColor = selected + ? theme.taskCard.selected.outlineColor + : theme.taskStatusOutline[status ?? TaskStatus.NULL]; + + const isOperator = [ + TaskType.FORK_JOIN_DYNAMIC, + TaskType.JOIN, + TaskType.FORK_JOIN, + TaskType.FORK_JOIN_DYNAMIC, + TaskType.TERMINATE, + TaskType.SUB_WORKFLOW, + TaskType.DYNAMIC, + TaskType.SET_VARIABLE, + TaskType.START_WORKFLOW, + ].includes(type); + + let cardStyles = {}; + + const operatorStyles = { + backgroundColor: theme.taskCard.operators.background, + border: outlineColor + ? `3px solid ${outlineColor}` + : `3px solid transparent`, + color: theme.taskCard.operators.text, + borderRadius: "10px", + }; + + const tasksStyles = { + backgroundColor: theme.taskCard.systemTasks.background, + color: theme.taskCard.systemTasks.color, + border: outlineColor + ? `3px solid ${outlineColor}` + : `3px solid transparent`, + borderRadius: "10px", + }; + + cardStyles = isOperator ? operatorStyles : tasksStyles; + + const errorStripesColor = () => { + if (isOperator) { + if (status === TaskStatus.CANCELED) { + return "rgba(251, 164, 4, .25)"; + } + return "rgba(90, 0, 0, .25)"; + } else { + if (status === TaskStatus.CANCELED) { + return "rgba(251, 164, 4, .15)"; + } + return "rgba(220, 110, 110, .15)"; + } + }; + + switch (status) { + case TaskStatus.FAILED: + case TaskStatus.SKIPPED: + case TaskStatus.CANCELED: + case TaskStatus.TIMED_OUT: + cardStyles = { + ...cardStyles, + backgroundImage: `linear-gradient( 135deg, rgba(0,0,0,0) 25%, ${errorStripesColor()} 25%, ${errorStripesColor()} 50%, rgba(0,0,0,0) 50%, rgba(0,0,0,0) 75%, ${errorStripesColor()} 75%, ${errorStripesColor()} 100% )`, + // These are not magic numbers!, see: https://css-tricks.com/no-jank-css-stripes/ + backgroundSize: "56.57px 56.57px", + }; + break; + + default: + break; + } + + const boxShadows = [TaskType.SWITCH, TaskType.DECISION].includes(type) + ? [] + : ["0 2px 20px rgba(0,0,0,.4)"]; + if (selected) { + boxShadows.push(theme.taskCard.selected.boxShadow); + } + + cardStyles = { + ...cardStyles, + boxShadow: boxShadows.join(", "), + }; + + return cardStyles; +}; diff --git a/ui-next/src/components/flow/components/shapes/testDiagrams.js b/ui-next/src/components/flow/components/shapes/testDiagrams.js new file mode 100644 index 0000000000..1148f89cc3 --- /dev/null +++ b/ui-next/src/components/flow/components/shapes/testDiagrams.js @@ -0,0 +1,708 @@ +export const simpleDiagram = { + updateTime: 1646331692036, + name: "image_convert_resize_jim", + description: "Image Processing Workflow", + version: 1, + tasks: [ + { + name: "image_convert_resize_jim", + taskReferenceName: "image_convert_resize_ref", + inputParameters: { + fileLocation: "${workflow.input.fileLocation}", + outputFormat: "${workflow.input.recipeParameters.outputFormat}", + outputWidth: "${workflow.input.recipeParameters.outputSize.width}", + outputHeight: "${workflow.input.recipeParameters.outputSize.height}", + }, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "upload_toS3_jim", + taskReferenceName: "upload_toS3_ref", + inputParameters: { + fileLocation: "${image_convert_resize_ref.output.fileLocation}", + }, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + inputParameters: [], + outputParameters: { + fileLocation: "${upload_toS3_ref.output.fileLocation}", + }, + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: true, + ownerEmail: "devrel@orkes.io", + timeoutPolicy: "ALERT_ONLY", + timeoutSeconds: 0, + failureWorkflow: "", + variables: {}, + inputTemplate: {}, +}; + +export const populationMinMax = { + updateTime: 1645990260050, + name: "PopulationMinMax", + description: "Min Max Population", + version: 1, + tasks: [ + { + name: "get_population_data", + taskReferenceName: "get_population_data_ref", + inputParameters: { + http_request: { + uri: "https://datausa.io/api/data?drilldowns=State&measures=Population&year=latest", + method: "GET", + }, + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "fork_join", + taskReferenceName: "fork_ref", + inputParameters: {}, + type: "FORK_JOIN", + decisionCases: {}, + defaultCase: [], + forkTasks: [ + [ + { + name: "process_population_max", + taskReferenceName: "process_population_max_ref", + inputParameters: { + body: "${get_population_data_ref.output.response.body}", + queryExpression: "[.body.data[]] | max_by(.Population)", + }, + type: "JSON_JQ_TRANSFORM", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + [ + { + name: "process_population_min", + taskReferenceName: "process_population_min_ref", + inputParameters: { + body: "${get_population_data_ref.output.response.body}", + queryExpression: "[.body.data[]] | min_by(.Population)", + }, + type: "JSON_JQ_TRANSFORM", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + ], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "join", + taskReferenceName: "join_ref", + inputParameters: {}, + type: "JOIN", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: ["process_population_max_ref", "process_population_min_ref"], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + inputParameters: [], + outputParameters: { + maxPopulation: "${process_population_max_ref.output.result}", + minPopulation: "${process_population_min_ref.output.result}", + }, + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: false, + ownerEmail: "developers@orkes.io", + timeoutPolicy: "ALERT_ONLY", + timeoutSeconds: 0, + failureWorkflow: "", + variables: {}, + inputTemplate: {}, +}; + +export const decisionSample = { + updateTime: 1636597950018, + name: "exclusive_join", + description: "Exclusive Join Example", + version: 1, + tasks: [ + { + type: "TERMINAL", + name: "start", + taskReferenceName: "__start", + }, + { + name: "api_decision", + taskReferenceName: "api_decision_ref", + inputParameters: { + case_value_param: "${workflow.input.type}", + }, + type: "DECISION", + caseValueParam: "case_value_param", + decisionCases: { + POST: [ + { + name: "get_posts", + taskReferenceName: "get_posts_ref", + inputParameters: { + http_request: { + uri: "https://jsonplaceholder.typicode.com/posts/1", + method: "GET", + }, + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + COMMENT: [ + { + name: "get_post_comments", + taskReferenceName: "get_post_comments_ref", + inputParameters: { + http_request: { + uri: "https://jsonplaceholder.typicode.com/comments?postId=1", + method: "GET", + }, + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + USER: [ + { + name: "get_user_posts", + taskReferenceName: "get_user_posts_ref", + inputParameters: { + http_request: { + uri: "https://jsonplaceholder.typicode.com/posts?userId=1", + method: "GET", + }, + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + }, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "notification_join", + taskReferenceName: "notification_join_ref", + inputParameters: {}, + type: "EXCLUSIVE_JOIN", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: ["get_posts_ref", "get_post_comments_ref"], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + type: "TERMINAL", + name: "final", + taskReferenceName: "__final", + }, + ], + inputParameters: [], + outputParameters: {}, + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: true, + ownerEmail: "encode_admin@test.com", + timeoutPolicy: "ALERT_ONLY", + timeoutSeconds: 0, + failureWorkflow: "", + variables: {}, + inputTemplate: {}, +}; + +export const complexDiagram = { + createTime: 1639691367677, + updateTime: 1641859692443, + name: "port_in_wf", + description: "Port In Workflow", + version: 1, + tasks: [ + { + type: "TERMINAL", + name: "start", + taskReferenceName: "__start", + }, + { + name: "Submit To ITG with Retry", + taskReferenceName: "submit_to_itg_with_retry", + inputParameters: { + value: "${workflow.input.iterations}", + terminate: "${workflow.variables.terminate_loop}", + }, + type: "DO_WHILE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopCondition: + "if ( ($.submit_to_itg_with_retry['iteration'] < $.value) && !$.terminate) { true; } else { false; }", + loopOver: [ + { + name: "Submit to ITG", + taskReferenceName: "submit_to_itg", + inputParameters: { + http_request: { + uri: "https://jsonplaceholder.typicode.com/todos/${$.workflow.input.iterations}", + method: "GET", + }, + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: true, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "Check Status", + taskReferenceName: "check_status", + inputParameters: { + prev_task_result: "${submit_to_itg.output}", + switchCaseValue: "${submit_to_itg.status}", + }, + type: "DECISION", + caseValueParam: "switchCaseValue", + decisionCases: { + COMPLETED: [ + { + name: "Complete Request Loop", + taskReferenceName: "complete_loop_success", + inputParameters: { + terminate_loop: true, + success: true, + }, + type: "SET_VARIABLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + COMPLETED_WITH_ERRORS: [ + { + name: "Retry HTTP Request", + taskReferenceName: "retry_http_request", + inputParameters: { + terminate_loop: false, + success: false, + }, + type: "SET_VARIABLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "Update Records", + taskReferenceName: "update_records_on_retry", + inputParameters: { + update_records_on_retry: 1, + }, + type: "SET_VARIABLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + }, + defaultCase: [ + { + name: "Permanent Failure", + taskReferenceName: "terminate_loop", + inputParameters: { + terminate_loop: true, + success: false, + }, + type: "SET_VARIABLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "Update Records Terminate", + taskReferenceName: "update_records_on_failure", + inputParameters: { + update_records_on_retry: 1, + }, + type: "SET_VARIABLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "Terminate Workflow", + taskReferenceName: "terminate_on_perm_failure", + inputParameters: { + terminationStatus: "FAILED", + workflowOutput: + "Failing workflow as retries exhausted and failures are marked as permenant", + }, + type: "TERMINATE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + }, + { + name: "Check If Success", + taskReferenceName: "check_success", + inputParameters: { + switchCaseValue: "${workflow.variables.success}", + }, + type: "DECISION", + caseValueParam: "switchCaseValue", + decisionCases: { + false: [ + { + name: "Update Records on Failure", + taskReferenceName: "update_records_on_failure", + inputParameters: { + update_records_on_retry: 2, + }, + type: "SET_VARIABLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "Terminate Workflow", + taskReferenceName: "terminate_on_perm_failure2", + inputParameters: { + terminationStatus: "FAILED", + workflowOutput: + "Failing workflow as retries exhausted and failures are marked as permenant", + }, + type: "TERMINATE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + }, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "Wait for the async message response", + taskReferenceName: "wait_for_response", + inputParameters: {}, + type: "WAIT", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "Check Response", + taskReferenceName: "check_response_succeeded", + inputParameters: { + switchCaseValue: "${wait_for_response.output.success}", + }, + type: "DECISION", + caseValueParam: "switchCaseValue", + decisionCases: { + false: [ + { + name: "Update Records on ITGH Failure", + taskReferenceName: "update_records_on_itg_failure", + inputParameters: { + response: "${wait_for_response.output}", + }, + type: "SET_VARIABLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "Terminate Workflow", + taskReferenceName: "terminate_on_response_failure", + inputParameters: { + terminationStatus: "FAILED", + workflowOutput: + "Failing workflow as retries exhausted and failures are marked as permenant", + }, + type: "TERMINATE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + }, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + type: "TERMINAL", + name: "final", + taskReferenceName: "__final", + }, + ], + inputParameters: [], + outputParameters: {}, + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: false, + ownerEmail: "example@email.com", + timeoutPolicy: "ALERT_ONLY", + timeoutSeconds: 0, + failureWorkflow: "", + variables: { + success: false, + }, + inputTemplate: {}, +}; + +export const allTaskTypes = { + updateTime: 1646331692036, + name: "all_task_types", + description: "All Task Types", + version: 1, + tasks: [ + { + name: "JSON JQ Transform Example", + taskReferenceName: "json_jq_transform_example_ref", + inputParameters: { + body: "${get_population_data_ref.output.response.body}", + queryExpression: "[.body.data[]] | max_by(.Population)", + }, + type: "JSON_JQ_TRANSFORM", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "inline_task_example", + taskReferenceName: "inline_task_example", + type: "INLINE", + inputParameters: { + value: "${workflow.input.value}", + evaluatorType: "graaljs", + expression: + 'function e() { if ($.value == 1){return {"result": true}} else { return {"result": false}}} e();', + }, + }, + { + name: "Kafka Task Example", + taskReferenceName: "call_kafka", + inputParameters: { + kafka_request: { + topic: "userTopic", + value: "Message to publish", + bootStrapServers: "localhost:9092", + headers: { + "x-Auth": "Auth-key", + }, + key: { + Key_1: "value 1", + }, + keySerializer: + "org.apache.kafka.common.serialization.IntegerSerializer", + }, + }, + type: "KAFKA_PUBLISH", + }, + ], + inputParameters: [], + outputParameters: { + fileLocation: "${upload_toS3_ref.output.fileLocation}", + }, + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: true, + ownerEmail: "devrel@orkes.io", + timeoutPolicy: "ALERT_ONLY", + timeoutSeconds: 0, + failureWorkflow: "", + variables: {}, + inputTemplate: {}, +}; diff --git a/ui-next/src/components/flow/dragDrop/DraggableOverlay.tsx b/ui-next/src/components/flow/dragDrop/DraggableOverlay.tsx new file mode 100644 index 0000000000..875ba2eaaf --- /dev/null +++ b/ui-next/src/components/flow/dragDrop/DraggableOverlay.tsx @@ -0,0 +1,73 @@ +import { DragOverlay, useDndContext } from "@dnd-kit/core"; +import { useSelector } from "@xstate/react"; +import { PanAndZoomMachineContext } from "components/flow/components/graphs/PanAndZoomWrapper/state"; +import { FlowContext, FlowEvents } from "components/flow/state"; +import { FunctionComponent, useMemo } from "react"; +import { ActorRef, State } from "xstate"; +import { + Shape, + ShapeComponentForTypeParams, +} from "../components/shapes/TaskShape/Shape"; + +export interface DragOverlayProps { + flowActor: ActorRef; +} + +export const DraggableOverlay: FunctionComponent = ({ + flowActor, +}) => { + const { active } = useDndContext(); + const draggedElement = useSelector( + flowActor, + (state: State) => state.context.draggedNodeData, + ); + // @ts-ignore + const panAndZoomActor = flowActor.children?.get("panAndZoomMachine"); + + return panAndZoomActor ? ( + } + active={!!active} + draggedElement={draggedElement} + /> + ) : null; +}; + +interface DraggableOverlayWithPanZoomProps { + panAndZoomActor: ActorRef; + active: boolean; + draggedElement: any; +} + +const DraggableOverlayWithPanZoom: FunctionComponent< + DraggableOverlayWithPanZoomProps +> = ({ panAndZoomActor, active, draggedElement }) => { + const scaleFactor = useSelector( + panAndZoomActor, + (state: State) => state.context.zoom, + ); + const shapeScaleStyles = useMemo( + () => ({ + transformOrigin: "top left", + transform: `scale(${scaleFactor})`, + opacity: 0.5, + }), + [scaleFactor], + ); + + return ( + + {active && draggedElement != null ? ( + {}} + style={shapeScaleStyles} + /> + ) : null} + + ); +}; diff --git a/ui-next/src/components/flow/dragDrop/Handle.tsx b/ui-next/src/components/flow/dragDrop/Handle.tsx new file mode 100644 index 0000000000..886a7d9742 --- /dev/null +++ b/ui-next/src/components/flow/dragDrop/Handle.tsx @@ -0,0 +1,104 @@ +import React, { forwardRef, CSSProperties } from "react"; +import { styled } from "@mui/system"; + +export interface ActionProps extends React.HTMLAttributes { + active?: { + fill: string; + background: string; + }; + cursor?: CSSProperties["cursor"]; +} + +const HandleButton = styled("button")` + position: absolute; + left: 0; + z-index: 3; + display: flex; + width: 12px; + padding: 15px; + align-items: center; + border: none; + justify-content: center; + flex: 0 0 auto; + touch-action: none; + cursor: var(--cursor, pointer); + border-radius: 5px; + outline: none; + appearance: none; + background-color: transparent; + -webkit-tap-highlight-color: transparent; + + @media (hover: hover) { + &:hover { + background-color: var(--action-background, rgba(0, 0, 0, 0.05)); + + svg { + fill: #6f7b88; + } + } + } + + svg { + flex: 0 0 auto; + margin: auto; + height: 100%; + overflow: visible; + fill: #919eab; + } + + &:active { + background-color: var(--background, rgba(0, 0, 0, 0.05)); + + svg { + fill: var(--fill, #788491); + } + } + + &:focus-visible { + outline: none; + box-shadow: + 0 0 0 2px rgba(255, 255, 255, 0), + 0 0px 0px 2px #4c9ffe; + } +`; + +export const Action = forwardRef( + ({ active, className, cursor, style, ...props }, ref) => { + return ( + + ); + }, +); + +export const Handle = forwardRef( + (props, ref) => { + return ( + + + + + + ); + }, +); diff --git a/ui-next/src/components/flow/dragDrop/boxCollision.ts b/ui-next/src/components/flow/dragDrop/boxCollision.ts new file mode 100644 index 0000000000..2242e207e6 --- /dev/null +++ b/ui-next/src/components/flow/dragDrop/boxCollision.ts @@ -0,0 +1,101 @@ +import { + Active, + CollisionDetection, + ClientRect, + CollisionDescriptor, +} from "@dnd-kit/core"; +import { ActorRef } from "xstate"; +import { useSelector } from "@xstate/react"; +import { PanAndZoomEvents } from "../components/graphs/PanAndZoomWrapper/state/types"; + +export function sortCollisionsDesc( + { data: { value: a } }: CollisionDescriptor, + { data: { value: b } }: CollisionDescriptor, +) { + return b - a; +} + +/** + * Returns the intersecting rectangle area between two rectangles + */ +function getIntersectionRatio(entry: ClientRect, active: Active): number { + const { + top: currentTop = 0, + left: currentLeft = 0, + width: currentWidth = 0, + height: currentHeight = 0, + } = active.rect.current.translated ?? {}; + + const top = Math.max(currentTop, entry.top); + const left = Math.max(currentLeft, entry.left); + const right = Math.min(currentLeft + currentWidth, entry.left + entry.width); + const bottom = Math.min(currentTop + currentHeight, entry.top + entry.height); + const width = right - left; + const height = bottom - top; + + if (left < right && top < bottom) { + const targetArea = currentWidth * currentHeight; + const entryArea = entry.width * entry.height; + const intersectionArea = width * height; + const intersectionRatio = + intersectionArea / (targetArea + entryArea - intersectionArea); + + return Number(intersectionRatio.toFixed(4)); + } + + // Rectangles do not overlap, or overlap has an area of zero (edge/corner overlap) + return 0; +} + +/** + * Returns the rectangle that has the greatest intersection area with a given + * rectangle in an array of rectangles. + */ +const performantRectIntersection = (useDom = false) => { + const activeRectIntersection: CollisionDetection = ({ + active, + droppableContainers, + }) => { + let maxIntersectionRatio = 0; + const collisions: CollisionDescriptor[] = []; + for (const droppableContainer of droppableContainers) { + const { id } = droppableContainer; + const { + rect: { current: rect }, + } = droppableContainer; + + if (rect) { + // Workaround to account for the movement of the position. + const actualRect = useDom + ? droppableContainer.node.current?.getBoundingClientRect() || rect + : rect; + const intersectionRatio = getIntersectionRatio(actualRect, active); + + if (intersectionRatio > maxIntersectionRatio) { + maxIntersectionRatio = intersectionRatio; + collisions.push({ + id, + data: { droppableContainer, value: intersectionRatio }, + }); + } + } + } + + return collisions.sort(sortCollisionsDesc); + }; + return activeRectIntersection; +}; + +export const useNodeCollisionDetection = ( + panAndZoomActor: ActorRef, +) => { + /** + * This is a workaround to account for the movement of the position. + * we don't want to hit the dom if the user has not dragged passed his position. Else we hit the dom. + */ + const useDom = useSelector( + panAndZoomActor!, + (state) => state.context.draggingUpdatedPosition, + ); + return performantRectIntersection(useDom); +}; diff --git a/ui-next/src/components/flow/dragDrop/hooks.ts b/ui-next/src/components/flow/dragDrop/hooks.ts new file mode 100644 index 0000000000..b5ecf268cb --- /dev/null +++ b/ui-next/src/components/flow/dragDrop/hooks.ts @@ -0,0 +1,181 @@ +import { useDraggable, useDroppable } from "@dnd-kit/core"; +import { useSelector } from "@xstate/react"; +import { + PanAndZoomContext, + PanAndZoomMachineContext, + PanAndZoomStates, +} from "components/flow/components/graphs/PanAndZoomWrapper/state"; +import { + NodeTaskData, + isSubWorkflowChild, + isTaskNext, + isTaskReferenceNestedInTaskReference, + previousTaskCrumb, +} from "components/flow/nodes/mapper"; +import { + DropPosition, + FlowContext, + FlowMachineStates, +} from "components/flow/state"; +import fastDeepEqual from "fast-deep-equal"; +import { useContext, useMemo } from "react"; +import { CommonTaskDef, TaskType } from "types"; +import type { State } from "xstate"; +import { FlowActorContext } from "../state/FlowActorContext"; + +interface DragDropNodeProps { + nodeData: NodeTaskData & { selected?: boolean }; + width?: number; + height?: number; + nodeId: string; +} + +const useIsPanEnabled = () => { + const { panAndZoomActor } = useContext(PanAndZoomContext); + const panIsEnabled = useSelector( + panAndZoomActor!, + (state: State) => + state.matches([ + PanAndZoomStates.IDLE, + PanAndZoomStates.PAN, + PanAndZoomStates.PAN_ENABLED, + ]), + ); + return panIsEnabled; +}; + +const useFlowContext = () => { + // Make this two seperate hooks + const { flowActor } = useContext(FlowActorContext); + const draggedNodeData = useSelector( + flowActor!, + (state: State) => state.context.draggedNodeData, + ); + const canDrag = useSelector(flowActor!, (state: State) => + state.matches([ + [ + FlowMachineStates.INIT, + FlowMachineStates.DIAGRAM_RENDERER, + FlowMachineStates.DIAGRAM_RENDERER_INIT, + FlowMachineStates.DIAGRAM_RENDERER_MENU_CLOSED, + ], + ]), + ); + return { draggedNodeData, canDrag }; +}; + +const DRAG_RESTRICTED_TASKS = [TaskType.SWITCH_JOIN, TaskType.TERMINAL]; + +const isNodeDataAJoinAfterAFork = (nodeData?: NodeTaskData): boolean => { + if (nodeData?.task.type === TaskType.JOIN) { + const previousCrumb = previousTaskCrumb( + nodeData.crumbs, + nodeData.task.taskReferenceName, + ); + return ( + previousCrumb !== undefined && + [TaskType.FORK_JOIN, TaskType.FORK_JOIN_DYNAMIC].includes( + previousCrumb.type, + ) + ); + } + return false; +}; + +export const useDraggableNode = ({ + nodeData, + width, + height, + nodeId, +}: DragDropNodeProps): { + draggableResult: ReturnType; + dragIsDisabled: boolean; +} => { + const panIsEnabled = useIsPanEnabled(); + const { canDrag } = useFlowContext(); + const dragIsDisabled = useMemo(() => { + // Determine if its execution by looking at the task data + const isExecution = nodeData?.task?.executionData != null; + return ( + isExecution || + panIsEnabled || + canDrag || + DRAG_RESTRICTED_TASKS.includes(nodeData?.task?.type) || + isNodeDataAJoinAfterAFork(nodeData) || + isSubWorkflowChild(nodeData?.crumbs, nodeData?.task?.taskReferenceName) + ); + }, [panIsEnabled, nodeData, canDrag]); + + const draggableResult = useDraggable({ + id: + nodeData.task.type === TaskType.SWITCH_JOIN + ? `${nodeId}_switch_join` + : nodeId, + data: { + ...nodeData, + height, + width, + }, + disabled: dragIsDisabled, + }); + return { draggableResult, dragIsDisabled }; +}; + +const isJoinAfterFork = ( + nodeData: NodeTaskData, + draggedTask?: CommonTaskDef, +) => { + if (draggedTask == null) return false; + if ( + [TaskType.FORK_JOIN, TaskType.FORK_JOIN_DYNAMIC].includes( + draggedTask.type, + ) && + nodeData.task.type === TaskType.JOIN + ) { + return isTaskNext( + nodeData.crumbs, + draggedTask.taskReferenceName, + nodeData.task.taskReferenceName, + ); + } + return false; +}; + +export const useDroppableNode = ({ + nodeData, + position, + nodeId, +}: DragDropNodeProps & DropPosition) => { + const panIsEnabled = useIsPanEnabled(); + const { draggedNodeData } = useFlowContext(); + const dropIsDisabled = useMemo(() => { + const targetTaskReferenceName = + nodeData.task.type === TaskType.SWITCH_JOIN && + nodeData.originalTask?.taskReferenceName + ? nodeData.originalTask?.taskReferenceName + : nodeData.task.taskReferenceName; + + if ( + panIsEnabled || + isJoinAfterFork(nodeData, draggedNodeData?.task) || + (draggedNodeData != null && + isTaskReferenceNestedInTaskReference( + nodeData.crumbs, + targetTaskReferenceName, + draggedNodeData.task.taskReferenceName, + )) || + fastDeepEqual(nodeData.crumbs, draggedNodeData?.crumbs) + ) { + return true; + } + return false; + }, [panIsEnabled, draggedNodeData, nodeData]); + + const droppableResult = useDroppable({ + id: nodeId, + data: { ...nodeData, position }, + disabled: dropIsDisabled, + }); + + return { droppableResult, draggedNodeData, dropIsDisabled }; +}; diff --git a/ui-next/src/components/flow/dragDrop/index.ts b/ui-next/src/components/flow/dragDrop/index.ts new file mode 100644 index 0000000000..2208aba8f1 --- /dev/null +++ b/ui-next/src/components/flow/dragDrop/index.ts @@ -0,0 +1,4 @@ +export * from "./DraggableOverlay"; +export * from "./hooks"; +export * from "./Handle"; +export * from "./boxCollision"; diff --git a/ui-next/src/components/flow/nodes/constants.js b/ui-next/src/components/flow/nodes/constants.js new file mode 100644 index 0000000000..abe4e25155 --- /dev/null +++ b/ui-next/src/components/flow/nodes/constants.js @@ -0,0 +1 @@ +export const MAX_EXPAND_TASKS = 2; diff --git a/ui-next/src/components/flow/nodes/index.js b/ui-next/src/components/flow/nodes/index.js new file mode 100644 index 0000000000..9ee943cc60 --- /dev/null +++ b/ui-next/src/components/flow/nodes/index.js @@ -0,0 +1,22 @@ +import { + workflowToNodeEdges as processWorkflow, + PORT_NORTH, + PORT_SOUTH, + crumbsToTask, + crumbsToTaskSteps, + START_TASK_FAKE_TASK_REFERENCE_NAME, + END_TASK_FAKE_TASK_REFERENCE_NAME, +} from "./mapper"; + +// This line should not be here, but it is: +export * from "../components/RichAddTaskMenu/taskGenerator"; + +export { + processWorkflow, + PORT_NORTH, + PORT_SOUTH, + START_TASK_FAKE_TASK_REFERENCE_NAME, + END_TASK_FAKE_TASK_REFERENCE_NAME, + crumbsToTask, + crumbsToTaskSteps, +}; diff --git a/ui-next/src/components/flow/nodes/layoutTestData.js b/ui-next/src/components/flow/nodes/layoutTestData.js new file mode 100644 index 0000000000..fd41623223 --- /dev/null +++ b/ui-next/src/components/flow/nodes/layoutTestData.js @@ -0,0 +1,116 @@ +export const oneLoopOneLevelDeep = { + nodes: [ + { + id: "__start", + type: "default", + data: { + label: "__start", + }, + position: { + x: 0, + y: 20, + }, + }, + { + id: "my_fork_join_ref", + type: "default", + data: { + label: "my_fork_join_ref", + }, + position: { + x: 0, + y: 20, + }, + }, + { + id: "loop_1", + type: "default", + data: { + label: "loop_1", + }, + style: { + width: 410, + height: 300, + }, + }, + { + id: "loop_1_task_iter", + type: "default", + data: { + label: "loop_1_task_iter", + }, + position: { + x: 0, + y: 0, + }, + parentNode: "loop_1", + extent: "parent", + }, + { + id: "loop_1_sv", + type: "default", + data: { + label: "loop_1_sv", + }, + position: { + x: 0, + y: 0, + }, + parentNode: "loop_1", + extent: "parent", + }, + { + id: "fork_join_ref", + type: "default", + data: { + label: "fork_join_ref", + }, + position: { + x: 0, + y: 20, + }, + }, + { + id: "__final", + type: "default", + data: { + label: "__final", + }, + position: { + x: 0, + y: 20, + }, + }, + ], + edges: [ + { + id: "edge___start-my_fork_join_ref", + source: "__start", + target: "my_fork_join_ref", + type: "smoothstep", + }, + { + id: "edge_my_fork_join_ref-loop_1", + source: "my_fork_join_ref", + target: "loop_1", + }, + { + id: "edge_loop_1_task_iter-loop_1_sv", + source: "loop_1_task_iter", + target: "loop_1_sv", + type: "smoothstep", + zIndex: 100, + }, + { + id: "edge_fork_join_ref-__final", + source: "fork_join_ref", + target: "__final", + type: "smoothstep", + }, + { + id: "edge_jt_loop_1-fork_join_ref", + source: "loop_1", + target: "fork_join_ref", + }, + ], +}; diff --git a/ui-next/src/components/flow/nodes/mapper/common.test.ts b/ui-next/src/components/flow/nodes/mapper/common.test.ts new file mode 100644 index 0000000000..882f9d586f --- /dev/null +++ b/ui-next/src/components/flow/nodes/mapper/common.test.ts @@ -0,0 +1,105 @@ +import { SimpleTaskDef, TaskStatus, TaskType } from "types"; +import { maybeEdgeData } from "./common"; + +describe("maybeEdgeData", () => { + const imageResizeTask: SimpleTaskDef = { + name: "image_convert_resize_jim", + taskReferenceName: "image_convert_resize_ref", + inputParameters: { + fileLocation: "${workflow.input.fileLocation}", + outputFormat: "${workflow.input.recipeParameters.outputFormat}", + outputWidth: "${workflow.input.recipeParameters.outputSize.width}", + outputHeight: "${workflow.input.recipeParameters.outputSize.height}", + }, + type: TaskType.SIMPLE, + optional: false, + }; + const uploadImageTask: SimpleTaskDef = { + name: "upload_toS3_jim", + taskReferenceName: "upload_toS3_ref", + inputParameters: { + fileLocation: "${image_convert_resize_ref.output.fileLocation}", + }, + type: TaskType.SIMPLE, + optional: false, + }; + + it("Should return status completed if both previous task and current task is complete", () => { + const edges = maybeEdgeData( + { + ...imageResizeTask, + executionData: { + status: TaskStatus.COMPLETED, + executed: true, + attempts: 0, + }, + }, + { + ...uploadImageTask, + executionData: { + status: TaskStatus.COMPLETED, + executed: true, + attempts: 0, + }, + }, + ); + expect(edges).toEqual({ + data: { + status: "COMPLETED", + unreachableEdge: false, + }, + }); + }); + it("Should return empty if the next task is PENDING", () => { + const edges = maybeEdgeData( + { + ...imageResizeTask, + executionData: { + status: TaskStatus.PENDING, + executed: true, + attempts: 0, + }, + }, + { + ...uploadImageTask, + executionData: { + status: TaskStatus.COMPLETED, + executed: true, + attempts: 0, + }, + }, + ); + expect(edges).toEqual({ + data: { + unreachableEdge: false, + }, + }); + }); + + it("Should return completed. if the first task is completed and the next task is FAILED", () => { + const edges = maybeEdgeData( + { + ...imageResizeTask, + executionData: { + status: TaskStatus.FAILED, + executed: true, + attempts: 0, + }, + }, + { + ...uploadImageTask, + executionData: { + status: TaskStatus.COMPLETED, + executed: true, + attempts: 0, + }, + }, + ); + expect(edges).toEqual({ + data: { + status: "COMPLETED", + unreachableEdge: false, + }, + }); + }); +}); diff --git a/ui-next/src/components/flow/nodes/mapper/common.ts b/ui-next/src/components/flow/nodes/mapper/common.ts new file mode 100644 index 0000000000..7ea524e8e7 --- /dev/null +++ b/ui-next/src/components/flow/nodes/mapper/common.ts @@ -0,0 +1,92 @@ +import { southPort } from "./ports"; +import _flow from "lodash/flow"; +import _last from "lodash/last"; +import _property from "lodash/property"; +import { taskToSize } from "./layout"; + +import { Crumb, CommonTaskDef, TaskStatus } from "types"; +import { NodeData } from "reaflow"; +import { NodeTaskData } from "./types"; + +export const extractTaskReference: (t: CommonTaskDef) => string = + _property("taskReferenceName"); + +export const extractLastTaskReferenceFn = _flow([_last, extractTaskReference]); + +export const extractExecutionDataOrEmpty = ( + task?: CommonTaskDef & { executionData?: any }, +) => (task?.executionData == null ? {} : task.executionData); + +export const taskHasCompleted = ( + task?: CommonTaskDef, + consideredCompletedStatus = [ + TaskStatus.COMPLETED, + TaskStatus.COMPLETED_WITH_ERRORS, + ], +) => + consideredCompletedStatus.includes(extractExecutionDataOrEmpty(task)?.status); + +export const taskIsPending = ( + task?: CommonTaskDef, + consideredPendingTaskStatus = [TaskStatus.PENDING], +) => + consideredPendingTaskStatus.includes( + extractExecutionDataOrEmpty(task)?.status, + ); + +export const completedTaskStatusData = ( + unreachableEdge = false, + delayedEdge?: boolean, +) => ({ + status: TaskStatus.COMPLETED, + unreachableEdge, + delayedEdge, +}); + +export const maybeEdgeData = ( + currentTask: CommonTaskDef, + previousTask?: CommonTaskDef, + unreachableEdge = false, + delayedEdge?: boolean, +) => { + const previousStatusIsCompleted = taskHasCompleted(previousTask); + + const previousAndCurrentStatusCompleted = + previousStatusIsCompleted && !taskIsPending(currentTask); + + return previousAndCurrentStatusCompleted + ? { + data: completedTaskStatusData(unreachableEdge, delayedEdge), + } + : { + data: { unreachableEdge, delayedEdge }, + }; +}; + +export const edgeIdMapper = ( + { taskReferenceName: sourceTaskReferenceName }: CommonTaskDef, + { taskReferenceName: destinationTaskReferenceName }: CommonTaskDef, +) => `edge_${sourceTaskReferenceName}-${destinationTaskReferenceName}`; + +export const taskToNode = ( + task: T, + crumbs: Crumb[] = [], + additionalProps = {}, +): NodeData => { + const { taskReferenceName, name } = task; + const { width, height } = taskToSize(task); + + return { + id: taskReferenceName, + text: name, + ...{ ports: [southPort({ id: taskReferenceName })] }, + data: { + task, + crumbs, + ...additionalProps, + ...extractExecutionDataOrEmpty(task), + }, + width, + height, + }; +}; diff --git a/ui-next/src/components/flow/nodes/mapper/constants.ts b/ui-next/src/components/flow/nodes/mapper/constants.ts new file mode 100644 index 0000000000..4d98475e13 --- /dev/null +++ b/ui-next/src/components/flow/nodes/mapper/constants.ts @@ -0,0 +1 @@ +export const TERMINAL_END_NAME = "end"; diff --git a/ui-next/src/components/flow/nodes/mapper/core.test.js b/ui-next/src/components/flow/nodes/mapper/core.test.js new file mode 100644 index 0000000000..44d4e99938 --- /dev/null +++ b/ui-next/src/components/flow/nodes/mapper/core.test.js @@ -0,0 +1,67 @@ +import { workflowToNodeEdges } from "./core"; +import { + simpleDiagram, + populationMinMax, + loanBanking, + simpleLoopSample, + nestedForkJoin, +} from "../../../../testData/diagramTests"; + +const nodesToMap = (nodes) => + nodes.reduce((acc, node) => ({ ...acc, [node.id]: node }), {}); + +const allEdgesAreConnectedToNodes = (edges, nodeMap) => + edges.every((edge) => { + if (nodeMap[edge.from] && nodeMap[edge.to]) { + return true; + } + //console.log(JSON.stringify(edge, null, 2)); + return false; + }); + +describe("workflowToNodeEdges", () => { + it("should convert a workflow to a list of edges", async () => { + const simpleDiagramNodesEdges = await workflowToNodeEdges(simpleDiagram); + const nodeMap = nodesToMap(simpleDiagramNodesEdges.nodes); + + expect( + allEdgesAreConnectedToNodes(simpleDiagramNodesEdges.edges, nodeMap), + ).toBe(true); + }); + + it("should convert a workflow with population min/max to a list of edges", async () => { + const populationMinMaxNodesEdges = + await workflowToNodeEdges(populationMinMax); + const nodeMap = nodesToMap(populationMinMaxNodesEdges.nodes); + + expect( + allEdgesAreConnectedToNodes(populationMinMaxNodesEdges.edges, nodeMap), + ).toBe(true); + }); + + it("should convert a workflow with a loop to a list of edges", async () => { + const simpleLoopSampleNodesEdges = + await workflowToNodeEdges(simpleLoopSample); + const nodeMap = nodesToMap(simpleLoopSampleNodesEdges.nodes); + expect( + allEdgesAreConnectedToNodes(simpleLoopSampleNodesEdges.edges, nodeMap), + ).toBe(true); + }); + + it("should convert a workflow with a nested fork join to a list of edges", async () => { + const loanBankingNodesAndEdges = await workflowToNodeEdges(loanBanking); + const nodeMap = nodesToMap(loanBankingNodesAndEdges.nodes); + expect( + allEdgesAreConnectedToNodes(loanBankingNodesAndEdges.edges, nodeMap), + ).toBe(true); + }); + + it("should convert a workflow with a loan banking to a list of edges", async () => { + const nestedForkJoinNodesAndEdges = + await workflowToNodeEdges(nestedForkJoin); + const nodeMap = nodesToMap(nestedForkJoinNodesAndEdges.nodes); + expect( + allEdgesAreConnectedToNodes(nestedForkJoinNodesAndEdges.edges, nodeMap), + ).toBe(true); + }); +}); diff --git a/ui-next/src/components/flow/nodes/mapper/core.ts b/ui-next/src/components/flow/nodes/mapper/core.ts new file mode 100644 index 0000000000..2b1870d5e4 --- /dev/null +++ b/ui-next/src/components/flow/nodes/mapper/core.ts @@ -0,0 +1,373 @@ +import _property from "lodash/property"; +import _first from "lodash/first"; +import _isUndefined from "lodash/isUndefined"; +import _mapValues from "lodash/mapValues"; +import { edgeMapper } from "./edgeMapper"; +import { taskToSwitchNodesEdges } from "./switch"; +import { taskToNode, maybeEdgeData } from "./common"; +import { taskToForkJoinNodesEdges } from "./forkJoin"; +import { processDoWhile } from "./doWhile"; +import { taskToTerminateNode } from "./terminate"; +import { taskToForkJoinDynamicNodesEdges } from "./forkJoinDynamic"; +import { joinTasksToNodesEdges } from "./join"; +import { processSubWorkflow } from "./subWorkflow"; +import { NodeData } from "reaflow"; +import { + processLastTask, + endNode, + startNode, + firstTask as firstFakeTask, +} from "./terminal"; +import { + CommonTaskDef, + TaskType, + Crumb, + WorkflowDef, + TaskStatus, + WorkflowExecutionStatus, +} from "types"; +import { NodeTaskData, EdgeTaskData, SubWorkflowFunction } from "./types"; + +import { + isJoinTask, + isForkJoinTask, + isForkJoinDynamicTask, + isDoWhileTask, + isTerminateTask, + isSubWorkflowTask, + isSwitchTask, + isForkableTask, +} from "./predicates"; + +export const extractTaskReferenceName = (tasks: { + taskReferenceName: string; +}) => Object.values(tasks).map(_property("taskReferenceName")); + +type Accumulator = { + nodes: NodeData[]; + edges: EdgeTaskData[]; + crumbs: Crumb[]; + previousTask?: CommonTaskDef; + previousTaskAllowsConnection: boolean; // deprecated +}; +type TasksAsNodesProps = { + tasks?: NodeData[]; + edges?: EdgeTaskData[]; + crumbs?: Crumb[]; + crumbContext?: Partial; + expandSubWorkflow?: boolean; + subWorkFlowFetcher?: SubWorkflowFunction; + readOnly?: boolean; +}; + +type TaskWalkerFn = ( + t: CommonTaskDef[], + tanProps: TasksAsNodesProps, +) => Promise; + +const mergeCur = (destination: Accumulator) => (source: Partial) => + ({ ...destination, ...source }) as Accumulator; +export const tasksAsNodes: TaskWalkerFn = async ( + mappableTasks: CommonTaskDef[], + { + tasks: initialTasks = [], + edges: initialEdges = [], + crumbs: initialCrumbs = [], + crumbContext = { + parent: null, + }, + expandSubWorkflow = true, + subWorkFlowFetcher = async (_workflowName: string, _version?: number) => + Promise.resolve({ tasks: [] }), + readOnly = false, + }: TasksAsNodesProps = { + tasks: [], + edges: [], + crumbs: [], + crumbContext: { + parent: null, + }, + readOnly: false, + }, +) => { + let acc: Accumulator = { + nodes: initialTasks, + edges: initialEdges, + previousTask: undefined, + crumbs: initialCrumbs, + previousTaskAllowsConnection: false, + }; + + for (const [idx, currentTask] of mappableTasks.entries()) { + const { type, taskReferenceName } = currentTask; + + const crumbs = acc.crumbs.concat({ + ...crumbContext, + ref: taskReferenceName, + refIdx: idx, + type, + }); + + let processedResult: Accumulator = { + nodes: acc.nodes, + edges: acc.edges, + previousTask: currentTask, + crumbs, + previousTaskAllowsConnection: true, + }; + + const updatePr = mergeCur(processedResult); + // task walker with current subworkflow props + const taskWalkerFunc: TaskWalkerFn = (tasksP, tanProps) => + tasksAsNodes(tasksP, { + expandSubWorkflow, + subWorkFlowFetcher, + readOnly, + ...tanProps, + }); + + if (isJoinTask(currentTask)) { + if (acc.previousTask) { + const previousTask = acc.previousTask; + const { nodes: joinNodes, edges: joinEdges } = joinTasksToNodesEdges( + currentTask, + previousTask, + crumbs, + acc.nodes, + ); + + // Update joinOn to point to the last node + // if the previous node was joined + if (isForkableTask(previousTask) && previousTask?.forkTasks?.length) { + previousTask.forkTasks.forEach((forkTask) => { + if (forkTask.length < 2) return; + const lastForkTask = forkTask[forkTask.length - 1]; + const nodeBeforeLastForkTask = forkTask[forkTask.length - 2]; + const isPreviousNodeJoinedOn = currentTask.joinOn.includes( + nodeBeforeLastForkTask.taskReferenceName, + ); + if (!isPreviousNodeJoinedOn) return; + currentTask.joinOn = currentTask.joinOn.map((joinOnRefName) => { + return joinOnRefName === nodeBeforeLastForkTask.taskReferenceName + ? lastForkTask.taskReferenceName + : joinOnRefName; + }); + }); + } + + processedResult = updatePr({ + nodes: joinNodes, + edges: acc.edges.concat( + edgeMapper( + currentTask, + previousTask, + acc.previousTaskAllowsConnection, + ), + joinEdges, + ), + }); + } else { + // Join is the first task + processedResult = updatePr({ + nodes: acc.nodes.concat(taskToNode(currentTask, crumbs)), + }); + } + } else if (isForkJoinTask(currentTask)) { + const { nodes: procesedForkedNodes, edges: procesedForkedEdges } = + await taskToForkJoinNodesEdges(currentTask, crumbs, taskWalkerFunc); + + processedResult = updatePr({ + nodes: acc.nodes.concat(procesedForkedNodes), + edges: acc.edges.concat( + edgeMapper( + currentTask, + acc?.previousTask, + acc.previousTaskAllowsConnection, + ), + procesedForkedEdges, + ), + }); + } else if (isForkJoinDynamicTask(currentTask)) { + const { nodes: forkJoinDynamicNodes, edges: forkJoinDynamicEdges } = + await taskToForkJoinDynamicNodesEdges( + currentTask, + crumbs, + taskWalkerFunc, + ); + + processedResult = updatePr({ + nodes: acc.nodes.concat(forkJoinDynamicNodes), + edges: acc.edges.concat( + edgeMapper( + currentTask, + acc?.previousTask, + acc?.previousTaskAllowsConnection, + ), + forkJoinDynamicEdges, + ), + }); + } else if (isSwitchTask(currentTask)) { + const { + nodes: switchNodes, + edges: switchEdges, + everyTaskIsTerminate, + } = await taskToSwitchNodesEdges(currentTask, crumbs, taskWalkerFunc); + + processedResult = updatePr({ + nodes: acc.nodes.concat(switchNodes), + edges: acc.edges.concat( + edgeMapper( + currentTask, + acc?.previousTask, + acc?.previousTaskAllowsConnection, + ), + switchEdges, + ), + previousTask: currentTask, + previousTaskAllowsConnection: !everyTaskIsTerminate, + }); + } else if (isDoWhileTask(currentTask)) { + const { nodes: doWhileNodes, edges: doWhileEdges } = await processDoWhile( + currentTask, + crumbs, + taskWalkerFunc, + ); + processedResult = updatePr({ + nodes: acc.nodes.concat(doWhileNodes), + edges: acc.edges + .concat( + edgeMapper( + currentTask, + acc?.previousTask, + acc?.previousTaskAllowsConnection, + ), + ) + .concat(doWhileEdges), + }); + } else if (isTerminateTask(currentTask)) { + processedResult = updatePr({ + nodes: acc.nodes.concat(taskToTerminateNode(currentTask, crumbs)), + edges: acc.edges.concat( + edgeMapper( + currentTask, + acc?.previousTask, + acc?.previousTaskAllowsConnection, + ), + ), + previousTask: currentTask, + previousTaskAllowsConnection: false, + }); + } else if (isSubWorkflowTask(currentTask) && expandSubWorkflow) { + const { nodes: subWorkflowNodes, edges: subWorkflowEdges } = + await processSubWorkflow( + currentTask, + crumbs, + tasksAsNodes, // We don't want to mantain the subworkflow props since we want this only on the outer layer + subWorkFlowFetcher, + ); + + processedResult = updatePr({ + nodes: acc.nodes.concat(subWorkflowNodes), + edges: acc.edges + .concat( + edgeMapper( + currentTask, + acc?.previousTask, + acc?.previousTaskAllowsConnection, + ), + ) + .concat(subWorkflowEdges), + }); + } else { + processedResult = updatePr({ + nodes: acc.nodes.concat(taskToNode(currentTask, crumbs)), + edges: acc.edges.concat( + edgeMapper( + currentTask, + acc?.previousTask, + acc?.previousTaskAllowsConnection, + ), + ), + }); + } + + acc = processedResult; + } + + return acc; +}; + +const maybePrependFirstNode = ( + { nodes, edges }: { nodes: NodeData[]; edges: EdgeTaskData[] }, + firstTask: CommonTaskDef, +) => { + const firstNode = nodes.find(({ id }) => id === firstTask.taskReferenceName); + return firstTask.type === TaskType.TERMINAL + ? { nodes, edges } + : { + nodes: [startNode].concat(nodes), + edges: [ + { + id: `edge_start_${startNode.id}_${firstNode?.id}`, + from: startNode.id, + to: firstNode?.id, + fromPort: `${startNode.id}-south-port`, + toPort: `${firstNode?.id}-to`, + ...maybeEdgeData(firstTask, { + ...firstFakeTask, + executionData: + firstTask?.executionData != null + ? { status: TaskStatus.COMPLETED } + : undefined, + }), + }, + ...edges, + ], + }; +}; + +export const workflowToNodeEdges = async ( + workflow: Partial, + showPorts = true, + expandSubWorkflow = true, + workflowFetcher: SubWorkflowFunction, + workflowStatus: WorkflowExecutionStatus, +) => { + const mappableTasks = workflow?.tasks || []; + if (mappableTasks.length < 1) { + return { + nodes: [startNode, endNode], + edges: [ + { + id: `edge_start_${startNode.id}_${endNode.id}`, + from: startNode.id, + to: endNode.id, + fromPort: `${startNode.id}-south-port`, + toPort: `${endNode.id}-to`, + }, + ], + }; + } + const firstTask = _first(mappableTasks); + const taskAsNodesResult = await tasksAsNodes(mappableTasks, { + subWorkFlowFetcher: workflowFetcher, + expandSubWorkflow, + readOnly: !showPorts, + }); + + const result = maybePrependFirstNode( + processLastTask(taskAsNodesResult, workflowStatus), + firstTask!, + ); + + return showPorts + ? result + : _mapValues(result, (arr) => + arr.map(({ ports, ...values }: any) => ({ + ...values, + ports: _isUndefined(ports) + ? undefined + : ports.map((p: any) => ({ ...p, hidden: true })), + })), + ); +}; diff --git a/ui-next/src/components/flow/nodes/mapper/crumbs.test.ts b/ui-next/src/components/flow/nodes/mapper/crumbs.test.ts new file mode 100644 index 0000000000..9abbe99736 --- /dev/null +++ b/ui-next/src/components/flow/nodes/mapper/crumbs.test.ts @@ -0,0 +1,864 @@ +import { + crumbsToTask, + isTaskReferenceNestedInTaskReference, + isTaskNext, + previousTaskCrumb, + isSubWorkflowChild, +} from "./crumbs"; +import { + simpleDiagram, + populationMinMax, + loanBanking, + simpleLoopSample, + nestedForkJoin, +} from "../../../../testData/diagramTests"; +import { TaskDef, Crumb, TaskType } from "types"; + +describe("crumbsToTask", () => { + it("Should return undefined if crumbs or task is empty", () => { + const result1 = crumbsToTask([], []); + expect(result1).toBeUndefined(); + + const taskReferenceName = "image_convert_resize_ref"; + const result2 = crumbsToTask( + [], + simpleDiagram.tasks as unknown as TaskDef[], + ); + expect(result2).toBeUndefined(); + + const crumbs: Crumb[] = [ + { + parent: undefined, + ref: taskReferenceName, + refIdx: 0, + type: TaskType.SIMPLE, + }, + ]; + + const result3 = crumbsToTask(crumbs, []); + + expect(result3).toBeUndefined(); + }); + it("Should return the task in a linear workflow", () => { + const taskReferenceName = "image_convert_resize_ref"; + const crumbs: Crumb[] = [ + { + parent: undefined, + ref: taskReferenceName, + refIdx: 0, + type: TaskType.SIMPLE, + }, + ]; + const result = crumbsToTask( + crumbs, + simpleDiagram.tasks as unknown as TaskDef[], + ); + expect(result!.taskReferenceName).toEqual(taskReferenceName); + }); + it("Should return the task if task is within fork", () => { + const taskReferenceName = "process_population_max_ref"; + const crumbs: Crumb[] = [ + { + parent: undefined, + ref: "get_population_data_ref", + refIdx: 0, + type: TaskType.SIMPLE, + }, + { + parent: undefined, + ref: "fork_ref", + refIdx: 1, + type: TaskType.FORK_JOIN, + }, + { + parent: "fork_ref", + ref: "process_population_max_ref", + refIdx: 0, + type: TaskType.SIMPLE, + }, + ]; + const result = crumbsToTask( + crumbs, + populationMinMax.tasks as unknown as TaskDef[], + ); + expect(result!.taskReferenceName).toEqual(taskReferenceName); + }); + it("Should work if task is within a switch path", () => { + const taskReferenceName = "employment_details_verification"; + const crumbs: Crumb[] = [ + { + parent: undefined, + ref: "customer_details", + refIdx: 0, + type: TaskType.SIMPLE, + }, + { + parent: undefined, + ref: "loan_type", + refIdx: 1, + type: TaskType.SIMPLE, + }, + { + parent: "loan_type", + decisionBranch: "property", + ref: "employment_details", + refIdx: 0, + type: TaskType.SWITCH, + }, + { + parent: "loan_type", + decisionBranch: "property", + ref: "employment_details_verification", + refIdx: 1, + type: TaskType.SIMPLE, + }, + ]; + const result = crumbsToTask( + crumbs, + loanBanking.tasks as unknown as TaskDef[], + ); + expect(result!.taskReferenceName).toEqual(taskReferenceName); + }); + it("Should work if task is within a switch defaultPath", () => { + const taskReferenceName = "business_details"; + const crumbs: Crumb[] = [ + { + parent: undefined, + ref: "customer_details", + refIdx: 0, + type: TaskType.SIMPLE, + }, + { + parent: undefined, + ref: "loan_type", + refIdx: 1, + type: TaskType.SWITCH, + }, + { + parent: "loan_type", + decisionBranch: "defaultCase", + ref: "business_details", + refIdx: 0, + type: TaskType.SIMPLE, + }, + ]; + const result = crumbsToTask( + crumbs, + loanBanking.tasks as unknown as TaskDef[], + ); + expect(result!.taskReferenceName).toEqual(taskReferenceName); + }); + + it("Should work if task is within a switch within a switch", () => { + const taskReferenceName = "loan_transfer_to_customer_account"; + const crumbs: Crumb[] = [ + { + ref: "customer_details", + refIdx: 0, + type: TaskType.SIMPLE, + }, + { + ref: "loan_type", + refIdx: 1, + type: TaskType.SIMPLE, + }, + { + ref: "credit_score_risk", + refIdx: 2, + type: TaskType.SIMPLE, + }, + { + ref: "credit_result", + refIdx: 3, + type: TaskType.SWITCH, + }, + { + parent: "credit_result", + decisionBranch: "possible", + ref: "principal_interest_calculation", + refIdx: 0, + type: TaskType.SIMPLE, + }, + { + parent: "credit_result", + decisionBranch: "possible", + ref: "customer_decision", + refIdx: 1, + type: TaskType.SIMPLE, + }, + { + parent: "customer_decision", + decisionBranch: "yes", + ref: "loan_transfer_to_customer_account", + refIdx: 0, + type: TaskType.SIMPLE, + }, + ]; + const result = crumbsToTask( + crumbs, + loanBanking.tasks as unknown as TaskDef[], + ); + expect(result!.taskReferenceName).toEqual(taskReferenceName); + }); + it("Should work if task is within a WHILE", () => { + const taskReferenceName = "loop_2_task_iter"; + const crumbs: Crumb[] = [ + { + ref: "__start", + refIdx: 0, + type: TaskType.TERMINAL, + }, + { + ref: "my_fork_join_ref", + refIdx: 1, + type: TaskType.FORK_JOIN, + }, + { + parent: "my_fork_join_ref", + forkIndex: 1, + ref: "loop_2", + refIdx: 0, + type: TaskType.DO_WHILE, + }, + { + parent: "loop_2", + ref: "loop_2_task_iter", + refIdx: 0, + type: TaskType.SIMPLE, + }, + ]; + const result = crumbsToTask( + crumbs, + simpleLoopSample.tasks as unknown as TaskDef[], + ); + expect(result!.taskReferenceName).toEqual(taskReferenceName); + }); + it("Should work for nested fork join within a switch", () => { + const taskReferenceName = "sample_task_name_join_uqholl_ref"; + const crumbs: Crumb[] = [ + { + ref: "get_random_fact", + refIdx: 0, + type: TaskType.SIMPLE, + }, + { + ref: "sample_task_name_fork_ytrlak_ref", + refIdx: 1, + + type: TaskType.SIMPLE, + }, + { + ref: "sample_task_name_join_fd9v1_ref", + refIdx: 2, + type: TaskType.SIMPLE, + }, + { + ref: "sample_task_name_http_mvwvv_ref", + refIdx: 3, + type: TaskType.SIMPLE, + }, + { + ref: "sample_task_name_join_a75or_ref", + refIdx: 4, + + type: TaskType.SIMPLE, + }, + { + ref: "sample_task_name_fork_6vg5rj_ref", + refIdx: 5, + + type: TaskType.SIMPLE, + }, + { + ref: "sample_task_name_join_6fc3tf_ref", + refIdx: 6, + + type: TaskType.SIMPLE, + }, + { + ref: "sample_task_name_switch_pm7wsj_ref", + refIdx: 7, + type: TaskType.SWITCH, + }, + { + parent: "sample_task_name_switch_pm7wsj_ref", + decisionBranch: "new_case_ms0jy", + ref: "sample_task_name_simple_0xdkv_ref", + refIdx: 0, + type: TaskType.SIMPLE, + }, + { + parent: "sample_task_name_switch_pm7wsj_ref", + decisionBranch: "new_case_ms0jy", + ref: "sample_task_name_fork_lx82h_ref", + refIdx: 1, + type: TaskType.SIMPLE, + }, + { + parent: "sample_task_name_switch_pm7wsj_ref", + decisionBranch: "new_case_ms0jy", + ref: taskReferenceName, + refIdx: 2, + type: TaskType.SIMPLE, + }, + ]; + const result = crumbsToTask( + crumbs, + nestedForkJoin.tasks as unknown as TaskDef[], + ); + expect(result!.taskReferenceName).toEqual(taskReferenceName); + }); +}); + +describe("isTaskReferenceNestedInTaskReference", () => { + it("Should return true if task is nested in a switch", () => { + const testCrumbs: Crumb[] = [ + { + parent: undefined, + ref: "get_random_fact", + refIdx: 0, + type: TaskType.SIMPLE, + }, + { + parent: undefined, + ref: "switch_task_l1bk1_ref", + refIdx: 1, + type: TaskType.SWITCH, + }, + { + parent: "switch_task_l1bk1_ref", + decisionBranch: "new_case_cxt61", + ref: "nested_http_ref", + type: TaskType.SIMPLE, + refIdx: 0, + }, + ]; + const nestedTaskReferenceName = "nested_http_ref"; + const maybeParent = "switch_task_l1bk1_ref"; + expect( + isTaskReferenceNestedInTaskReference( + testCrumbs, + nestedTaskReferenceName, + maybeParent, + ), + ).toEqual(true); + }); + + it("Should return true if task is nested in a fork", () => { + const testCrumbs: Crumb[] = [ + { + parent: undefined, + ref: "get_random_fact", + type: TaskType.SIMPLE, + refIdx: 0, + }, + { + parent: undefined, + ref: "fork_task_uglok_ref", + type: TaskType.FORK_JOIN, + refIdx: 1, + }, + { + parent: "fork_task_uglok_ref", + forkIndex: 0, + ref: "nested_event_ref", + refIdx: 0, + type: TaskType.EVENT, + }, + ]; + + const nestedTaskReferenceName = "nested_event_ref"; + const maybeParent = "fork_task_uglok_ref"; + + expect( + isTaskReferenceNestedInTaskReference( + testCrumbs, + nestedTaskReferenceName, + maybeParent, + ), + ).toEqual(true); + }); + + it("Should return true if task is nested in a doWhile", () => { + const testCrumbs: Crumb[] = [ + { + parent: undefined, + ref: "get_random_fact", + refIdx: 0, + type: TaskType.SIMPLE, + }, + { + parent: undefined, + ref: "http_poll_task_qikye_ref", + refIdx: 1, + type: TaskType.HTTP, + }, + { + parent: undefined, + ref: "do_while_task_iv18s_ref", + refIdx: 2, + type: TaskType.DO_WHILE, + }, + { + parent: "do_while_task_iv18s_ref", + ref: "nested_http_ref", + refIdx: 0, + type: TaskType.HTTP, + }, + ]; + + const nestedTaskReferenceName = "nested_http_ref"; + const maybeParent = "do_while_task_iv18s_ref"; + + expect( + isTaskReferenceNestedInTaskReference( + testCrumbs, + nestedTaskReferenceName, + maybeParent, + ), + ).toEqual(true); + }); + + it("Should return true if the task is nested within a nesgted parent for example a switch within a fork", () => { + const testCrumbs: Crumb[] = [ + { + parent: undefined, + ref: "get_random_fact", + refIdx: 0, + type: TaskType.SIMPLE, + }, + { + parent: undefined, + ref: "http_poll_task_qikye_ref", + refIdx: 1, + type: TaskType.HTTP, + }, + { + parent: undefined, + ref: "fork_task_9qlfc_ref", + refIdx: 2, + type: TaskType.FORK_JOIN, + }, + { + parent: "fork_task_9qlfc_ref", + forkIndex: 0, + ref: "switch_task_l2pcc_ref", + refIdx: 0, + type: TaskType.SWITCH, + }, + { + parent: "switch_task_l2pcc_ref", + decisionBranch: "new_case_e6vpy", + ref: "do_while_task_rth2u_ref", + refIdx: 0, + type: TaskType.DO_WHILE, + }, + { + parent: "do_while_task_rth2u_ref", + ref: "event_task_n2zld_ref", + refIdx: 0, + type: TaskType.EVENT, + }, + { + parent: "do_while_task_rth2u_ref", + ref: "double_nested_event_ref", + refIdx: 1, + type: TaskType.DO_WHILE, + }, + ]; + + const nestedTaskReferenceName = "double_nested_event_ref"; + const maybeParent = "fork_task_9qlfc_ref"; + + expect( + isTaskReferenceNestedInTaskReference( + testCrumbs, + nestedTaskReferenceName, + maybeParent, + ), + ).toEqual(true); + }); + it("Should return false if maybeParent is not the parent of nestedTaskReferenceName", () => { + const testCrumbs: Crumb[] = [ + { + parent: undefined, + ref: "http_task_ref", + refIdx: 0, + type: TaskType.HTTP, + }, + { + parent: undefined, + ref: "human_task_ref", + refIdx: 1, + type: TaskType.HUMAN, + }, + { + parent: undefined, + ref: "fork_task_ref", + refIdx: 2, + type: TaskType.FORK_JOIN, + }, + { + parent: "fork_task_ref", + forkIndex: 0, + ref: "event_task_ref", + refIdx: 0, + type: TaskType.EVENT, + }, + { + parent: "fork_task_ref", + forkIndex: 0, + ref: "switch_task_ref", + refIdx: 1, + type: TaskType.SWITCH, + }, + { + parent: "switch_task_ref", + decisionBranch: "defaultCase", + ref: "http_poll_task_ref_1", + refIdx: 0, + type: TaskType.HTTP, + }, + ]; + + const nestedTaskReferenceName = "http_poll_task_ref_1"; + const maybeParent = "kafka_publish_task_ref"; + + expect( + isTaskReferenceNestedInTaskReference( + testCrumbs, + nestedTaskReferenceName, + maybeParent, + ), + ).toEqual(false); + }); + it("Should return true. if nestedTaskReferenceName is nested in a task that is nested in maybeParent", () => { + const testCrumbs: Crumb[] = [ + { + parent: undefined, + ref: "fork_task_ref_1", + refIdx: 0, + type: TaskType.FORK_JOIN, + }, + { + parent: "fork_task_ref_1", + forkIndex: 0, + ref: "http_task_ref", + refIdx: 0, + type: TaskType.HTTP, + }, + { + parent: "fork_task_ref_1", + forkIndex: 0, + ref: "switch_task_ref", + refIdx: 1, + type: TaskType.SWITCH, + }, + { + parent: "switch_task_ref", + decisionBranch: "new_case_ilm6lf", + ref: "http_task_ref_3", + refIdx: 0, + type: TaskType.HTTP, + }, + { + parent: "switch_task_ref", + decisionBranch: "new_case_ilm6lf", + ref: "fork_task_ref_2", + refIdx: 1, + type: TaskType.FORK_JOIN, + }, + { + parent: "fork_task_ref_2", + forkIndex: 1, + ref: "http_task_ref_4", + refIdx: 0, + type: TaskType.HTTP, + }, + ]; + + const nestedTaskReferenceName = "http_task_ref_4"; + const maybeParent = "switch_task_ref"; + + expect( + isTaskReferenceNestedInTaskReference( + testCrumbs, + nestedTaskReferenceName, + maybeParent, + ), + ).toEqual(true); + }); +}); +describe("previousTaskCrumb", () => { + it("Should return the previous task crumb", () => { + const simpleForkJoinCrumbs: Crumb[] = [ + { + parent: undefined, + ref: "fork_task_ref_1", + refIdx: 0, + type: TaskType.FORK_JOIN, + }, + { + parent: undefined, + ref: "join_task_ref_1", + refIdx: 1, + type: TaskType.JOIN, + }, + ]; + const crumb = previousTaskCrumb(simpleForkJoinCrumbs, "join_task_ref_1"); + expect(crumb).toEqual(simpleForkJoinCrumbs[0]); + }); + it("should return previous task crumb in nested tree", () => { + const crumbs: Crumb[] = [ + { + parent: undefined, + ref: "fork_task_ref_1", + refIdx: 0, + type: TaskType.FORK_JOIN, + }, + { + parent: "fork_task_ref_1", + forkIndex: 0, + ref: "http_task_ref", + refIdx: 0, + type: TaskType.HTTP, + }, + { + parent: "fork_task_ref_1", + forkIndex: 0, + ref: "switch_task_ref", + refIdx: 1, + type: TaskType.SWITCH, + }, + { + parent: "switch_task_ref", + decisionBranch: "new_case_ilm6lf", + ref: "http_task_ref_3", + refIdx: 0, + type: TaskType.HTTP, + }, + { + parent: "switch_task_ref", + decisionBranch: "new_case_ilm6lf", + ref: "fork_task_ref_2", + refIdx: 1, + type: TaskType.FORK_JOIN, + }, + { + parent: "switch_task_ref", + decisionBranch: "new_case_ilm6lf", + ref: "join_task_ref_2", + refIdx: 2, + type: TaskType.JOIN, + }, + ]; + + const crumb = previousTaskCrumb(crumbs, "join_task_ref_2"); + expect(crumb).toEqual({ + parent: "switch_task_ref", + decisionBranch: "new_case_ilm6lf", + ref: "fork_task_ref_2", + refIdx: 1, + type: TaskType.FORK_JOIN, + }); + }); + it("should return undefined if task is the first task in tree", () => { + const crumbs: Crumb[] = [ + { + parent: undefined, + ref: "fork_task_ref_1", + refIdx: 0, + type: TaskType.FORK_JOIN, + }, + { + parent: "fork_task_ref_1", + forkIndex: 0, + ref: "http_task_ref", + refIdx: 0, + type: TaskType.HTTP, + }, + { + parent: "fork_task_ref_1", + forkIndex: 0, + ref: "switch_task_ref", + refIdx: 1, + type: TaskType.SWITCH, + }, + { + parent: "switch_task_ref", + decisionBranch: "new_case_ilm6lf", + ref: "http_task_ref_3", + refIdx: 0, + type: TaskType.HTTP, + }, + { + parent: "switch_task_ref", + decisionBranch: "new_case_ilm6lf", + ref: "fork_task_ref_2", + refIdx: 1, + type: TaskType.FORK_JOIN, + }, + { + parent: "switch_task_ref", + decisionBranch: "new_case_ilm6lf", + ref: "join_task_ref_2", + refIdx: 2, + type: TaskType.JOIN, + }, + ]; + + const crumb = previousTaskCrumb(crumbs, "http_task_ref"); + expect(crumb).toBeUndefined(); + }); +}); + +describe("isTaskNext", () => { + it("Should return true if the the second task is next in the tree", () => { + const crumbs: Crumb[] = [ + { + parent: undefined, + ref: "fork_task_ref_1", + refIdx: 0, + type: TaskType.FORK_JOIN, + }, + { + parent: "fork_task_ref_1", + forkIndex: 0, + ref: "http_task_ref", + refIdx: 0, + type: TaskType.HTTP, + }, + { + parent: "fork_task_ref_1", + forkIndex: 0, + ref: "switch_task_ref", + refIdx: 1, + type: TaskType.SWITCH, + }, + { + parent: "switch_task_ref", + decisionBranch: "new_case_ilm6lf", + ref: "http_task_ref_3", + refIdx: 0, + type: TaskType.HTTP, + }, + { + parent: "switch_task_ref", + decisionBranch: "new_case_ilm6lf", + ref: "fork_task_ref_2", + refIdx: 1, + type: TaskType.FORK_JOIN, + }, + { + parent: "switch_task_ref", + decisionBranch: "new_case_ilm6lf", + ref: "join_task_ref_2", + refIdx: 2, + type: TaskType.JOIN, + }, + ]; + expect(isTaskNext(crumbs, "http_task_ref", "switch_task_ref")).toBeTruthy(); + }); + it("Should return false if the second task is not next in the tree", () => { + const crumbs: Crumb[] = [ + { + parent: undefined, + ref: "fork_task_ref_1", + refIdx: 0, + type: TaskType.FORK_JOIN, + }, + { + parent: "fork_task_ref_1", + forkIndex: 0, + ref: "http_task_ref", + refIdx: 0, + type: TaskType.HTTP, + }, + { + parent: "fork_task_ref_1", + forkIndex: 0, + ref: "switch_task_ref", + refIdx: 1, + type: TaskType.SWITCH, + }, + { + parent: "switch_task_ref", + decisionBranch: "new_case_ilm6lf", + ref: "http_task_ref_3", + refIdx: 0, + type: TaskType.HTTP, + }, + { + parent: "switch_task_ref", + decisionBranch: "new_case_ilm6lf", + ref: "fork_task_ref_2", + refIdx: 1, + type: TaskType.FORK_JOIN, + }, + { + parent: "switch_task_ref", + decisionBranch: "new_case_ilm6lf", + ref: "join_task_ref_2", + refIdx: 2, + type: TaskType.JOIN, + }, + ]; + expect(isTaskNext(crumbs, "switch_task_ref", "http_task_ref")).toBeFalsy(); + }); +}); + +describe("disable drag for subworkflow child nodes", () => { + const crumbs: any = [ + { + parent: null, + ref: "http_task_ref", + refIdx: 0, + type: "HTTP", + }, + { + parent: null, + ref: "sub_workflow_task_ref", + refIdx: 1, + type: "SUB_WORKFLOW", + }, + { + parent: "sub_workflow_task_ref", + ref: "do_while_task_ref", + refIdx: 0, + type: "DO_WHILE", + }, + { + parent: "do_while_task_ref", + ref: "some_task_ref", + refIdx: 0, + type: "DO_WHILE", + }, + { + parent: "some_task_ref", + ref: "event_task_ref_2", + refIdx: 0, + type: "EVENT", + }, + { + parent: "sub_workflow_task_ref", + ref: "get_random_fact", + refIdx: 0, + type: "HTTP", + }, + { + parent: "sub_workflow_task_ref", + ref: "http_task_qlcyu_ref", + refIdx: 1, + type: "HTTP", + }, + ]; + + it("If a task is direct child to subworkflow", () => { + const result = isSubWorkflowChild(crumbs, "http_task_qlcyu_ref"); + expect(result).toBe(true); + }); + it("If a task is nested child to subworkflow", () => { + const result = isSubWorkflowChild(crumbs, "event_task_ref_2"); + expect(result).toBe(true); + }); +}); diff --git a/ui-next/src/components/flow/nodes/mapper/crumbs.ts b/ui-next/src/components/flow/nodes/mapper/crumbs.ts new file mode 100644 index 0000000000..e7220e09e9 --- /dev/null +++ b/ui-next/src/components/flow/nodes/mapper/crumbs.ts @@ -0,0 +1,248 @@ +import _findLast from "lodash/findLast"; +import _head from "lodash/head"; +import _isEmpty from "lodash/isEmpty"; +import _isNil from "lodash/isNil"; +import _last from "lodash/last"; +import _nth from "lodash/nth"; +import _tail from "lodash/tail"; +import { Crumb, TaskDef, TaskType } from "types"; + +const taskForCurrentCrumb = ( + crumb: Crumb, + tasks: TaskDef[], + parentTask?: TaskDef, +): TaskDef | undefined => { + if (_isNil(crumb?.parent)) { + const maybeTask = _nth(tasks, crumb?.refIdx); + if (maybeTask) { + return maybeTask; + } + } + + switch (parentTask?.type) { + case TaskType.FORK_JOIN: { + const { forkIndex, refIdx: forkRefIndex } = crumb!; + const forkTasks: TaskDef[][] = parentTask!.forkTasks!; + return _nth(_nth(forkTasks, forkIndex), forkRefIndex); + } + case TaskType.SWITCH: + case TaskType.DECISION: { + const { decisionBranch, refIdx: switchRefIndex } = crumb!; + const { decisionCases, defaultCase } = parentTask; + const isDefault = decisionBranch === "defaultCase"; + + const decisionCaseTasksAffected = isDefault + ? defaultCase + : decisionCases![decisionBranch!]!; + + return _nth(decisionCaseTasksAffected, switchRefIndex); + } + case TaskType.DO_WHILE: { + const { loopOver } = parentTask!; + return _nth(loopOver, crumb.refIdx); + } + default: { + return _nth(tasks, crumb.refIdx); + } + } +}; + +export const crumbsToTaskSteps = ( + crumbs: Crumb[], + tasks: TaskDef[], + taskSteps: TaskDef[] = [], + maybeParent?: TaskDef, +): TaskDef[] => { + const restCrumbs = _tail(crumbs); + const currentCrumb = _head(crumbs); + const parent = + maybeParent?.taskReferenceName === currentCrumb?.parent // parent was memorized use parent, else finde parent + ? maybeParent + : _findLast( + taskSteps, + (_ref) => _ref?.taskReferenceName === currentCrumb?.parent, + ); + + const task = taskForCurrentCrumb(currentCrumb!, tasks, parent); + if (_isEmpty(restCrumbs)) { + return task != null ? taskSteps.concat(task) : taskSteps; + } + return crumbsToTaskSteps(restCrumbs, tasks, taskSteps.concat(task!), parent); +}; + +export const crumbsToTask = ( + crumbs: Crumb[], + tasks: TaskDef[], +): TaskDef | undefined => { + return _isEmpty(crumbs) || _isEmpty(tasks) + ? undefined + : _last(crumbsToTaskSteps(crumbs, tasks)); +}; + +const applyFuncToIndexIfParent = ( + crumbs: Crumb[], + parent: string | null | undefined, + func: (crumb: Crumb) => Crumb, +) => { + return crumbs.map((crumb) => { + if (crumb.parent === parent) { + return func(crumb); + } + return crumb; + }); +}; + +export const removeTaskReferenceFromCrumbs = ( + crumbs: Crumb[], + taskReferenceName: string, +) => { + let newCrumbs: Crumb[] = []; + for (let i = 0; i < crumbs.length; i++) { + const currentCrumb = crumbs[i]; + if (currentCrumb.ref === taskReferenceName) { + return newCrumbs.concat( + applyFuncToIndexIfParent( + crumbs.slice(i + 1), + currentCrumb.parent, + (crumb) => ({ + ...crumb, + refIdx: crumb.refIdx - 1, + }), + ), + ); + } else { + newCrumbs = newCrumbs.concat(currentCrumb); + } + } + return newCrumbs; +}; + +export const isTaskReferenceNestedInAnyTaskReference = ( + crumbs: Crumb[], + targetTaskReference: string, + maybeParentTaskReferenceName: string[], +): boolean => { + const parentMap = new Map(); + for (let i = 0; i < crumbs.length; i++) { + const currentCrumb = crumbs[i]; + if (currentCrumb.parent != null) { + parentMap.set(currentCrumb.ref, currentCrumb.parent); + } + if (currentCrumb.ref === targetTaskReference) { + if (currentCrumb.parent != null) { + const doesCurrentCrumbParentHasTargetParent = + maybeParentTaskReferenceName.includes(currentCrumb.parent); + const doesCurrentCrumbParentHasParent = + parentMap.get(currentCrumb.parent) != null; + + const isParentOfParentTarget = + doesCurrentCrumbParentHasParent && + maybeParentTaskReferenceName.includes( + parentMap.get(currentCrumb.parent)!, + ); + + const parentOfParentIsNotTarget = () => + doesCurrentCrumbParentHasParent && + isTaskReferenceNestedInAnyTaskReference( + crumbs, + parentMap.get(currentCrumb.parent!)!, //we know its not null we've checked + maybeParentTaskReferenceName, + ); + + return ( + doesCurrentCrumbParentHasTargetParent || + isParentOfParentTarget || + parentOfParentIsNotTarget() + ); + } + } + } + return false; +}; +export const isTaskReferenceNestedInTaskReference = ( + crumbs: Crumb[], + targetTaskReference: string, + maybeParentTaskReferenceName: string, +): boolean => { + return isTaskReferenceNestedInAnyTaskReference(crumbs, targetTaskReference, [ + maybeParentTaskReferenceName, + ]); +}; + +/** + * Takes the crumb + * @param crumbs + * @param forkTaskReferenceName + * @param joinTaskReferenceName + */ +export const isTaskNext = ( + crumbs: Crumb[], + targetTaskReferenceFirst: string, + targetTaskReferenceSecond: string, +): boolean => { + const firstTaskIndex = crumbs.findIndex( + (crumb) => crumb.ref === targetTaskReferenceFirst, + ); + const secondTaskIndex = crumbs.findIndex( + (crumb) => crumb.ref === targetTaskReferenceSecond, + ); + if (firstTaskIndex === -1 || secondTaskIndex === -1) return false; + + const firstTaskCrumb = _nth(crumbs, firstTaskIndex); + const secondTaskCrumb = _nth(crumbs, secondTaskIndex); + if (firstTaskCrumb != null && secondTaskCrumb != null) { + const isSameParent = firstTaskCrumb?.parent === secondTaskCrumb?.parent; + + const isSecondTaskAfterFirstTask = + secondTaskCrumb.refIdx === firstTaskCrumb.refIdx + 1; + return isSameParent && isSecondTaskAfterFirstTask; + } + + return false; +}; + +/** + * Takes a crumbs list and a taskReference. will return the previous task crumb in the DAG tree + * @param crumbs + * @param taskReferenceName + * @returns + */ +export const previousTaskCrumb = ( + crumbs: Crumb[], + taskReferenceName: string, +): Crumb | undefined => { + const taskIndex = crumbs.findIndex( + (crumb) => crumb.ref === taskReferenceName, + ); + if (taskIndex === -1) return undefined; + const crumbAtIndex = _nth(crumbs, taskIndex); + if (crumbAtIndex !== undefined) { + const targetSlice = crumbs.slice(0, taskIndex); + const maybeElement = _findLast( + targetSlice, + (crumb) => + crumb.parent === crumbAtIndex.parent && + crumb.refIdx === crumbAtIndex.refIdx - 1, + ); + return maybeElement; + } + return undefined; +}; + +export const isSubWorkflowChild = ( + crumbs: Crumb[], + taskReferenceName: string, +): boolean => { + let availableSubworkflows; + if (crumbs) { + availableSubworkflows = crumbs + .filter((item) => item.type === TaskType.SUB_WORKFLOW) + .map((item) => item.ref); + return isTaskReferenceNestedInAnyTaskReference( + crumbs, + taskReferenceName, + availableSubworkflows, + ); + } + return false; +}; diff --git a/ui-next/src/components/flow/nodes/mapper/doWhile.ts b/ui-next/src/components/flow/nodes/mapper/doWhile.ts new file mode 100644 index 0000000000..1e09cf7410 --- /dev/null +++ b/ui-next/src/components/flow/nodes/mapper/doWhile.ts @@ -0,0 +1,77 @@ +import { northPort } from "./ports"; +import _first from "lodash/first"; +import _last from "lodash/last"; +import _identity from "lodash/identity"; +import _isNil from "lodash/isNil"; +import { taskToNode } from "./common"; +import { DoWhileTaskDef, Crumb, CommonTaskDef } from "types"; +import { NodeData, EdgeData } from "reaflow"; + +type DoWhileTaskDefWithMaybeExecutionData = DoWhileTaskDef & { + executionData?: any; +}; + +const maybeAddPortsToWhileNodes = (nodes: NodeData[]): NodeData[] => { + if (nodes.length === 0) { + return nodes; + } else if (nodes.length === 1) { + return nodes.map((n) => ({ ...n, ports: n.ports?.concat(northPort(n)) })); + } + + const firstNode = _first(nodes)!; + const lastNode = _last(nodes)!; + const noHeadNoTail = nodes.slice(1, -1); + + return [ + { ...firstNode, ports: firstNode.ports?.concat(northPort(firstNode)) }, + ...noHeadNoTail, + lastNode, + ]; +}; +type NodesEdgesAndCrumbs = { + nodes: NodeData[]; + edges: EdgeData[]; + crumbs: Crumb[]; +}; +export const processDoWhile = async ( + doWhileTask: DoWhileTaskDefWithMaybeExecutionData, + crumbs: Crumb[], + taskWalkerFn: any, +): Promise => { + const { loopOver, taskReferenceName, executionData } = doWhileTask; + + const loopOverNodesEdges = await taskWalkerFn(loopOver, { + crumbContext: { + parent: doWhileTask.taskReferenceName, + }, + crumbs, + }); + + const nodeMapper: (nodes: NodeData[]) => NodeData[] = + executionData == null ? maybeAddPortsToWhileNodes : _identity; + + return { + // TODO Fix when importing the sdk + nodes: [ + taskToNode(doWhileTask as CommonTaskDef, crumbs) as NodeData, + ].concat( + nodeMapper(loopOverNodesEdges!.nodes!).map((t) => + _isNil(t.parent) + ? { + ...t, + parent: taskReferenceName, + } + : t, + ), + ), + edges: loopOverNodesEdges.edges.map((e: EdgeData) => + _isNil(e.parent) + ? { + ...e, + parent: taskReferenceName, + } + : e, + ), + crumbs: crumbs.concat(loopOverNodesEdges.crumbs), + }; +}; diff --git a/ui-next/src/components/flow/nodes/mapper/edgeMapper.test.js b/ui-next/src/components/flow/nodes/mapper/edgeMapper.test.js new file mode 100644 index 0000000000..5b163d1e26 --- /dev/null +++ b/ui-next/src/components/flow/nodes/mapper/edgeMapper.test.js @@ -0,0 +1,210 @@ +import { edgeMapper } from "./edgeMapper"; + +describe("edgeMapper", () => { + const imageResizeTask = { + name: "image_convert_resize_jim", + taskReferenceName: "image_convert_resize_ref", + inputParameters: { + fileLocation: "${workflow.input.fileLocation}", + outputFormat: "${workflow.input.recipeParameters.outputFormat}", + outputWidth: "${workflow.input.recipeParameters.outputSize.width}", + outputHeight: "${workflow.input.recipeParameters.outputSize.height}", + }, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }; + const uploadImageTask = { + name: "upload_toS3_jim", + taskReferenceName: "upload_toS3_ref", + inputParameters: { + fileLocation: "${image_convert_resize_ref.output.fileLocation}", + }, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }; + const forkJoinTask = { + name: "fork_join", + taskReferenceName: "fork_ref", + inputParameters: {}, + type: "FORK_JOIN", + decisionCases: {}, + defaultCase: [], + forkTasks: [[]], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }; + const sampleJoinTask = { + name: "join", + taskReferenceName: "join_ref", + inputParameters: {}, + type: "JOIN", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: ["process_population_max_ref", "process_population_min_ref"], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }; + const terminateTask = { + name: "terminate_due_to_bank_rejection", + taskReferenceName: "terminate_due_to_bank_rejection", + inputParameters: { + terminationStatus: "COMPLETED", + }, + type: "TERMINATE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }; + + const sampleSwitchTask = { + name: "sample_task_name_switch_pm7wsj_ref", + taskReferenceName: "sample_task_name_switch_pm7wsj_ref", + inputParameters: { + switchCaseValue: "", + }, + type: "SWITCH", + decisionCases: { + new_case_ms0jy: [forkJoinTask, sampleJoinTask], + }, + defaultCase: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + }; + const switchWithTerminate = { + name: "sample_task_name_switch_pm7wsj_ref", + taskReferenceName: "sample_task_name_switch_pm7wsj_ref", + inputParameters: { + switchCaseValue: "", + }, + type: "SWITCH", + decisionCases: { + new_case_ms0jy: [forkJoinTask, sampleJoinTask], + }, + defaultCase: [terminateTask], + evaluatorType: "value-param", + expression: "switchCaseValue", + }; + + it("Should create the joining edge between two generic tasks", () => { + const edges = edgeMapper(imageResizeTask, uploadImageTask); + expect(edges.length).toBe(1); + expect(edges[0]).toEqual({ + id: "edge_upload_toS3_ref-image_convert_resize_ref", + from: "upload_toS3_ref", + fromPort: "upload_toS3_ref-south-port", + toPort: "image_convert_resize_ref-to", + to: "image_convert_resize_ref", + + data: { + unreachableEdge: false, + }, + }); + }); + + it("Should return empty if there is no previous task", () => { + const edges = edgeMapper(imageResizeTask, undefined); + expect(edges.length).toBe(0); + }); + + it("Should return empty if currentTask is type join and previous task is fork join", () => { + const edges = edgeMapper(sampleJoinTask, forkJoinTask); + expect(edges.length).toBe(0); + }); + + it("Should return empty if currentTask is type FORK_JOIN_DYNAMIC and previous task is fork join", () => { + const edges = edgeMapper(sampleJoinTask, { + ...forkJoinTask, + type: "FORK_JOIN_DYNAMIC", + }); + expect(edges.length).toBe(0); + }); + + it("Should connect with tasks that TERMINATE", async () => { + const edges = edgeMapper(sampleJoinTask, terminateTask, false); + expect(edges.length).toBe(1); + }); + + it("Should return a single edge connected to current task if previous task is switch and readOnly is false", () => { + const edges = edgeMapper(sampleJoinTask, sampleSwitchTask, true); + expect(edges).toEqual([ + { + id: "edge_sample_task_name_switch_pm7wsj_ref_switch_join-join_ref", + from: "sample_task_name_switch_pm7wsj_ref_switch_join", // Switch join connection + fromPort: + "switch_fake_task_sample_task_name_switch_pm7wsj_ref_switch_join-south-port", + toPort: "join_ref-to", + to: "join_ref", + + data: { + unreachableEdge: false, + }, + }, + ]); + }); + it("Should return an edge flagged with unreeachable if last task was a terminate task", () => { + const edges = edgeMapper(sampleJoinTask, switchWithTerminate, false); + expect(edges).toEqual([ + { + id: "edge_sample_task_name_switch_pm7wsj_ref_switch_join-join_ref", + from: "sample_task_name_switch_pm7wsj_ref_switch_join", // Switch join connection + fromPort: + "switch_fake_task_sample_task_name_switch_pm7wsj_ref_switch_join-south-port", + toPort: "join_ref-to", + to: "join_ref", + data: { + unreachableEdge: true, + }, + }, + ]); + }); + it("Should include the status of the edge if executionData is present. and both previous task and current task is complete", () => { + const edges = edgeMapper( + { ...imageResizeTask, executionData: { status: "COMPLETED" } }, + { ...uploadImageTask, executionData: { status: "COMPLETED" } }, + true, + ); + expect(edges).toEqual([ + { + id: "edge_upload_toS3_ref-image_convert_resize_ref", + from: "upload_toS3_ref", // Switch join connection + fromPort: "upload_toS3_ref-south-port", + toPort: "image_convert_resize_ref-to", + to: "image_convert_resize_ref", + data: { + status: "COMPLETED", + unreachableEdge: false, + }, + }, + ]); + }); +}); diff --git a/ui-next/src/components/flow/nodes/mapper/edgeMapper.ts b/ui-next/src/components/flow/nodes/mapper/edgeMapper.ts new file mode 100644 index 0000000000..2794be937f --- /dev/null +++ b/ui-next/src/components/flow/nodes/mapper/edgeMapper.ts @@ -0,0 +1,71 @@ +import { switchTaskToFakeNodeId, switchFakeTaskIDSouthPortId } from "./switch"; +import { maybeEdgeData } from "./common"; +import _isEmpty from "lodash/isEmpty"; +import { + TaskType, + SwitchTaskDef, + CommonTaskDef, + ForkJoinDynamicDef, +} from "types"; +import { isSwitchType } from "./predicates"; + +/** + * validates if previous task is connectable. returns true if it is + * @param previousTask + * @param previousTaskTerminatedNoEdges + * @returns + */ +const canConnectToPreviousTask = (previousTask?: CommonTaskDef) => + previousTask != null; + +export const edgeMapper = ( + currentTask: CommonTaskDef, + previousTask?: CommonTaskDef, + previousTaskAllowsConnection = true, +) => { + let sourceId = previousTask?.taskReferenceName; + let previousTaskSouthPortId = `${sourceId}-south-port`; + + const isForkJoinTaskPair = + currentTask.type === TaskType.JOIN && + previousTask?.type === TaskType.FORK_JOIN; + + if (isForkJoinTaskPair) return []; + + if ( + canConnectToPreviousTask(previousTask) && + previousTask?.type === TaskType.FORK_JOIN_DYNAMIC && + currentTask?.type === TaskType.JOIN && + !_isEmpty((previousTask as ForkJoinDynamicDef)?.forkTasks) + ) { + return []; + } + + const target = currentTask.taskReferenceName; + + if ( + canConnectToPreviousTask(previousTask) && + isSwitchType(previousTask?.type) + ) { + const previousSwitchTask = previousTask as SwitchTaskDef; + sourceId = switchTaskToFakeNodeId(previousSwitchTask); + previousTaskSouthPortId = switchFakeTaskIDSouthPortId(sourceId); + } + + if (sourceId == null) return []; + + return [ + { + id: `edge_${sourceId}-${currentTask.taskReferenceName}`, + from: sourceId, + fromPort: previousTaskSouthPortId, + toPort: `${target}-to`, + to: target, + ...maybeEdgeData( + currentTask, + previousTask, + !previousTaskAllowsConnection, + ), + }, + ]; +}; diff --git a/ui-next/src/components/flow/nodes/mapper/forkJoin.test.js b/ui-next/src/components/flow/nodes/mapper/forkJoin.test.js new file mode 100644 index 0000000000..edb549f74a --- /dev/null +++ b/ui-next/src/components/flow/nodes/mapper/forkJoin.test.js @@ -0,0 +1,11 @@ +import { tasksAsNodes } from "./core"; +import { processForkJoinTasks } from "./forkJoin"; +import { forkJoinTask } from "../../../../testData/diagramTests"; + +describe("processForkJoin", () => { + it("Should return nodes and edges for processForkJoin", async () => { + const result = await processForkJoinTasks(forkJoinTask, [], tasksAsNodes); + expect(result.edges.length).toEqual(forkJoinTask.forkTasks.length); // given that there is only one task per array + expect(result.nodes.length).toEqual(forkJoinTask.forkTasks.flat().length); + }); +}); diff --git a/ui-next/src/components/flow/nodes/mapper/forkJoin.ts b/ui-next/src/components/flow/nodes/mapper/forkJoin.ts new file mode 100644 index 0000000000..515bb5dec9 --- /dev/null +++ b/ui-next/src/components/flow/nodes/mapper/forkJoin.ts @@ -0,0 +1,99 @@ +import _head from "lodash/head"; +import _isEmpty from "lodash/isEmpty"; +import { southPort } from "./ports"; +import { + extractExecutionDataOrEmpty, + edgeIdMapper, + maybeEdgeData, +} from "./common"; +import { taskToSize } from "./layout"; // TODO maybe get rid of this. +import { ForkJoinTaskDef, Crumb, CommonTaskDef, ForkableTask } from "types"; +import { NodeData, EdgeData } from "reaflow"; +import { NodesAndEdges, NodeTaskData } from "./types"; + +export const innerTaskConnectingEdge = ( + taskHoldingTasks: CommonTaskDef, + processedInnerNodes: NodeData[], + suffix = "0", +): EdgeData => { + const firstTask = _head(processedInnerNodes)!.data.task; + return { + id: `${edgeIdMapper(taskHoldingTasks, firstTask)}_${suffix}`, + fromPort: `${taskHoldingTasks.taskReferenceName}_[key=${suffix}]-south-port`, + toPort: `${firstTask.taskReferenceName}-to`, + from: taskHoldingTasks.taskReferenceName, + to: firstTask.taskReferenceName, + ...maybeEdgeData(firstTask, taskHoldingTasks), + }; +}; +export const processForkJoinTasks = async ( + forkJoinTask: T, + crumbs: Crumb[], + taskWalkerFn: any, +): Promise => { + const { forkTasks = [] } = forkJoinTask; + let acc: NodesAndEdges = { + nodes: [], + edges: [], + }; + for (const [idx, innerTasks] of forkTasks.entries()) { + const { nodes, edges } = await taskWalkerFn(innerTasks, { + crumbContext: { + parent: forkJoinTask.taskReferenceName, + forkIndex: idx, + }, + crumbs, + }); + const maybeConnectingEdge: EdgeData[] = _isEmpty(nodes) + ? [] + : [innerTaskConnectingEdge(forkJoinTask, nodes, `${idx}`)]; + acc = { + edges: acc.edges.concat(maybeConnectingEdge, edges), + nodes: acc.nodes.concat(nodes), + }; + } + + return acc; +}; + +export const forkJoinTaskToNode = ( + task: T, + crumbs: Crumb[], +): NodeData> => { + const { taskReferenceName, name, forkTasks = [] } = task; + return { + id: taskReferenceName, + text: name, + ports: forkTasks.map((_, idx) => + southPort({ id: `${taskReferenceName}_[key=${idx}]` }, idx), + ), + data: { + task, + crumbs, + ...extractExecutionDataOrEmpty(task), + }, + ...taskToSize(task), + }; +}; + +export const taskToForkJoinNodesEdges = async ( + task: ForkJoinTaskDef, + crumbs: Crumb[], + taskWalkerFn: any, +) => { + const forkJoinNode = forkJoinTaskToNode(task, crumbs); + const { nodes: forkJoinInnerNodes, edges: forkJoinInnerEdges } = + await processForkJoinTasks(task, crumbs, taskWalkerFn); + + const initialElement: NodeData[] = [forkJoinNode]; + return { + nodes: initialElement.concat(forkJoinInnerNodes), + edges: forkJoinInnerEdges, + }; +}; + +export const isForkJoinPathEmpty = ( + forkIndex: number, + currentTask: ForkJoinTaskDef, +) => + forkIndex && currentTask && currentTask.forkTasks?.[forkIndex]?.length === 0; diff --git a/ui-next/src/components/flow/nodes/mapper/forkJoinDynamic.ts b/ui-next/src/components/flow/nodes/mapper/forkJoinDynamic.ts new file mode 100644 index 0000000000..8dd56a6045 --- /dev/null +++ b/ui-next/src/components/flow/nodes/mapper/forkJoinDynamic.ts @@ -0,0 +1,17 @@ +import { processForkJoinTasks, forkJoinTaskToNode } from "./forkJoin"; +import { ForkJoinDynamicDef, Crumb } from "types"; + +export const taskToForkJoinDynamicNodesEdges = async ( + task: ForkJoinDynamicDef, + crumbs: Crumb[], + taskWalkerFn: any, +) => { + const forkJoinDynamicNode = forkJoinTaskToNode(task, crumbs); + const { nodes: forkJoinInnerNodes, edges: forkJoinInnerEdges } = + await processForkJoinTasks(task, crumbs, taskWalkerFn); + + return { + nodes: [forkJoinDynamicNode, ...forkJoinInnerNodes], + edges: forkJoinInnerEdges, + }; +}; diff --git a/ui-next/src/components/flow/nodes/mapper/index.ts b/ui-next/src/components/flow/nodes/mapper/index.ts new file mode 100644 index 0000000000..0ef9fc05c5 --- /dev/null +++ b/ui-next/src/components/flow/nodes/mapper/index.ts @@ -0,0 +1,17 @@ +import { BOTTOM_PORT_MARGIN } from "./layout"; +import { + START_TASK_FAKE_TASK_REFERENCE_NAME, + END_TASK_FAKE_TASK_REFERENCE_NAME, +} from "./terminal"; + +export * from "./core"; +export * from "./ports"; +export * from "./join"; +export * from "./crumbs"; +export * from "./types"; + +export { + BOTTOM_PORT_MARGIN, + START_TASK_FAKE_TASK_REFERENCE_NAME, + END_TASK_FAKE_TASK_REFERENCE_NAME, +}; diff --git a/ui-next/src/components/flow/nodes/mapper/join.test.js b/ui-next/src/components/flow/nodes/mapper/join.test.js new file mode 100644 index 0000000000..169f1579c3 --- /dev/null +++ b/ui-next/src/components/flow/nodes/mapper/join.test.js @@ -0,0 +1,231 @@ +import { joinEdgeForSwitch, joinTasksToNodesEdges } from "./join"; + +describe("toJoinTaskToNodesEdgesFn", () => { + const joinTask = { + name: "join_task_9ysua_ref", + taskReferenceName: "join_task_9ysua_ref", + inputParameters: {}, + type: "JOIN", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }; + const anEventTask = { + name: "event_task_q3cxy_ref", + taskReferenceName: "event_task_q3cxy_ref", + type: "EVENT", + sink: "conductor:internal_event_name", + }; + + it("Should return a labeless edge since the task is after the switch", () => { + const switchTask = { + name: "switch_task_mjpgf_ref", + taskReferenceName: "switch_task_mjpgf_ref", + inputParameters: { + switchCaseValue: "", + }, + type: "SWITCH", + decisionCases: { + new_case_ceop1: [], + }, + defaultCase: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + }; + const forkTask = { + name: "fork_task_a3kx5_ref", + taskReferenceName: "fork_task_a3kx5_ref", + inputParameters: {}, + type: "FORK_JOIN", + decisionCases: {}, + defaultCase: [], + forkTasks: [[switchTask, anEventTask]], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }; + const { nodes, edges } = joinTasksToNodesEdges(joinTask, forkTask, [], []); + const joinNode = nodes[0]; + expect(joinNode.id).toBe(joinTask.taskReferenceName); + expect(edges.length).toBe(1); + expect(edges[0].from).toBe(anEventTask.taskReferenceName); + expect(edges[0].to).toBe(joinTask.taskReferenceName); + }); + it("Should add an unreachable task as an edge if the fork tasks array is empty", () => { + const forkTask = { + name: "fork_task_192fs", + taskReferenceName: "fork_task_192fs_ref", + inputParameters: {}, + type: "FORK_JOIN", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }; + const joinTask = { + name: "join_task_nc6vo", + taskReferenceName: "join_task_nc6vo_ref", + inputParameters: {}, + type: "JOIN", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }; + const { nodes, edges } = joinTasksToNodesEdges(joinTask, forkTask, [], []); + expect(nodes.length).toBe(1); + expect(edges.length).toBe(1); + expect(edges[0].from).toBe("fork_task_192fs_ref"); + expect(edges[0].to).toBe("join_task_nc6vo_ref"); + expect(edges[0].data.unreachableEdge).toBe(true); + }); + it("Should mark edge as delayed when task is not in joinOn", () => { + const switchTask = { + name: "switch_task", + taskReferenceName: "switch_task_ref", + type: "SWITCH", + defaultCase: [], + startDelay: 0, + optional: false, + asyncComplete: false, + }; + + const joinTask = { + name: "join_task", + taskReferenceName: "join_task_ref", + type: "JOIN", + joinOn: [], // Empty joinOn means switch task not included + startDelay: 0, + optional: false, + asyncComplete: false, + }; + + const result = joinEdgeForSwitch(switchTask, 0, joinTask); + + expect(result.joinOn.length).toBe(1); + expect(result.joinOn[0].data.delayedEdge).toBe(true); + }); + + it("Should not mark edge as delayed when task is in joinOn", () => { + const switchTask = { + name: "switch_task", + taskReferenceName: "switch_task_ref", + type: "SWITCH", + defaultCase: [], + startDelay: 0, + optional: false, + asyncComplete: false, + }; + + const joinTask = { + name: "join_task", + taskReferenceName: "join_task_ref", + type: "JOIN", + joinOn: ["switch_task_ref"], // Switch task included in joinOn + startDelay: 0, + optional: false, + asyncComplete: false, + }; + + const result = joinEdgeForSwitch(switchTask, 0, joinTask); + + expect(result.joinOn.length).toBe(1); + expect(result.joinOn[0].data.delayedEdge).toBe(false); + }); + it("Should mark edge as delayed in joinTasksToNodesEdges when task is not in joinOn", () => { + const forkTask = { + name: "fork_task", + taskReferenceName: "fork_task_ref", + type: "FORK_JOIN", + forkTasks: [ + [ + { + name: "inner_task", + taskReferenceName: "inner_task_ref", + type: "SIMPLE", + startDelay: 0, + optional: false, + asyncComplete: false, + }, + ], + ], + startDelay: 0, + optional: false, + asyncComplete: false, + }; + + const joinTask = { + name: "join_task", + taskReferenceName: "join_task_ref", + type: "JOIN", + joinOn: [], // Empty joinOn means inner task not included + startDelay: 0, + optional: false, + asyncComplete: false, + }; + + const result = joinTasksToNodesEdges(joinTask, forkTask, [], []); + + expect(result.edges.length).toBe(1); + expect(result.edges[0].data.delayedEdge).toBe(true); + }); + + it("Should not mark edge as delayed in joinTasksToNodesEdges when task is in joinOn", () => { + const forkTask = { + name: "fork_task", + taskReferenceName: "fork_task_ref", + type: "FORK_JOIN", + forkTasks: [ + [ + { + name: "inner_task", + taskReferenceName: "inner_task_ref", + type: "SIMPLE", + startDelay: 0, + optional: false, + asyncComplete: false, + }, + ], + ], + startDelay: 0, + optional: false, + asyncComplete: false, + }; + + const joinTask = { + name: "join_task", + taskReferenceName: "join_task_ref", + type: "JOIN", + joinOn: ["inner_task_ref"], // Inner task included in joinOn + startDelay: 0, + optional: false, + asyncComplete: false, + }; + + const result = joinTasksToNodesEdges(joinTask, forkTask, [], []); + + expect(result.edges.length).toBe(1); + expect(result.edges[0].data.delayedEdge).toBe(false); + }); +}); diff --git a/ui-next/src/components/flow/nodes/mapper/join.ts b/ui-next/src/components/flow/nodes/mapper/join.ts new file mode 100644 index 0000000000..20916ac618 --- /dev/null +++ b/ui-next/src/components/flow/nodes/mapper/join.ts @@ -0,0 +1,260 @@ +import { + extractTaskReference, + extractExecutionDataOrEmpty, + taskHasCompleted, + maybeEdgeData, +} from "./common"; +import { NodeData, EdgeData } from "reaflow"; +import { northPort, southPort, DiagramPort } from "./ports"; +import _isEmpty from "lodash/isEmpty"; +import _last from "lodash/last"; +import { + drillForEndTasks, + switchTaskToFakeNodeId, + switchFakeTaskIDSouthPortId, +} from "./switch"; +import { taskToSize } from "./layout"; +import { logger } from "utils/logger"; +import { isSwitchTask, isForkableTask } from "./predicates"; +import { + JoinTaskDef, + SwitchTaskDef, + Crumb, + CommonTaskDef, + TaskType, +} from "types"; + +import { NodesAndEdges, NodeTaskData } from "./types"; + +type JoinOnDirectPathsEdgesDiagramPorts = { + joinOn: EdgeData[]; + directPaths: EdgeData[]; + northPorts: DiagramPort[]; +}; + +export const forkLastTasks = async ( + tasks: CommonTaskDef[], + taskWalkerFn: any, +): Promise => { + const lastTask = _last(tasks); + + if (lastTask != null && isSwitchTask(lastTask)) { + const switchEndTaskDriller = drillForEndTasks(taskWalkerFn); + const tasksToConnect = await switchEndTaskDriller(lastTask, []); + + return _isEmpty(tasksToConnect) + ? [lastTask] + : tasksToConnect.filter( + ({ allowsTaskConnection }) => allowsTaskConnection, + ); + } + + return lastTask == null ? [] : [lastTask]; +}; + +export const forkLastTaskReferences = async ( + tasks: CommonTaskDef[], + taskWalkerFn: any, +): Promise => { + const lastTasks = await forkLastTasks(tasks, taskWalkerFn); + return lastTasks.map(extractTaskReference); +}; + +export const isTaskNotInJoinOn = ( + joinOn: string[] = [], + currentTaskRef: string, +): boolean => { + return !joinOn.includes(currentTaskRef); +}; + +export const joinEdgeForSwitch = ( + switchTask: SwitchTaskDef, + index: number, + joinTask: JoinTaskDef, +): JoinOnDirectPathsEdgesDiagramPorts => { + if (isSwitchTask(switchTask)) { + const fakeSwitchTaskId = switchTaskToFakeNodeId(switchTask); + const joinTaskReferenceName = joinTask.taskReferenceName; + const isDelayedEdge = isTaskNotInJoinOn( + joinTask.joinOn, + switchTask.taskReferenceName, + ); + return { + joinOn: [ + { + id: `edge_jj_is_${fakeSwitchTaskId}-${joinTaskReferenceName}`, + from: fakeSwitchTaskId, + fromPort: switchFakeTaskIDSouthPortId(fakeSwitchTaskId), + toPort: `${joinTaskReferenceName}-joinOnTask-${index}-north-port`, + to: joinTaskReferenceName, + // + ...(taskHasCompleted(switchTask) + ? { + data: { + status: "COMPLETED", + delayedEdge: isDelayedEdge, + }, + } + : { + data: { + delayedEdge: isDelayedEdge, + }, + }), + }, + ], + northPorts: [ + northPort( + { id: `${joinTaskReferenceName}-joinOnTask-${index}` }, + index, + true, + ), + ], + directPaths: [], + }; + } + + logger.warn( + "Expected switch task and got something else. Returning identity", + switchTask, + ); + + return { + joinOn: [], + northPorts: [], + directPaths: [], + }; +}; + +export const createJoinNode = ( + joinTask: JoinTaskDef, + crumbs: Crumb[], + previousTask?: CommonTaskDef, +) => { + const { width, height } = taskToSize(joinTask); + return { + id: joinTask.taskReferenceName, + text: joinTask.name, + ports: [southPort({ id: joinTask.taskReferenceName })], + data: { + task: joinTask, + crumbs, + previousTask, + // TODO fix when using sdk types + ...extractExecutionDataOrEmpty(joinTask as CommonTaskDef), + }, + width, + height, + } as const; +}; + +export const joinTasksToNodesEdges = ( + joinTask: JoinTaskDef, + previousTask: CommonTaskDef, + crumbs: Crumb[], + currentNodes: NodeData[], +): NodesAndEdges => { + const joinNode = createJoinNode(joinTask, crumbs, previousTask); + + let result: JoinOnDirectPathsEdgesDiagramPorts = { + joinOn: [], + directPaths: [], + northPorts: [], + }; + + if (isForkableTask(previousTask) && previousTask?.forkTasks != null) { + const { forkTasks, taskReferenceName: forkTaskReferenceName } = + previousTask; + // Special case there is no inner-array in forkTasks + if (_isEmpty(forkTasks) && previousTask.type === TaskType.FORK_JOIN) { + result = { + joinOn: result.joinOn, + northPorts: [], + directPaths: result.directPaths.concat({ + id: `edge_dp_${forkTaskReferenceName}_${joinTask.taskReferenceName}_0`, + from: forkTaskReferenceName, + to: joinTask.taskReferenceName, + ...maybeEdgeData(joinTask, previousTask, true), // mark as unreachable + }), + }; + } + + for (const [idx, tasks] of forkTasks.entries()) { + const invertedIndex = forkTasks.length - 1 - idx; + if (_isEmpty(tasks)) { + result = { + joinOn: result.joinOn, + northPorts: result.northPorts.concat( + northPort( + { id: `${joinNode.id}-direct-${invertedIndex}` }, + invertedIndex, + true, + ), + ), + directPaths: result.directPaths.concat({ + id: `edge_dp_${forkTaskReferenceName}_${joinTask.taskReferenceName}_${invertedIndex}`, + from: forkTaskReferenceName, + fromPort: `${forkTaskReferenceName}_[key=${idx}]-south-port`, + toPort: `${joinTask.taskReferenceName}-direct-${invertedIndex}-north-port`, + to: joinTask.taskReferenceName, + ...maybeEdgeData(joinTask, previousTask), + }), + }; + } else { + const maybeLastTask = _last(tasks); + if (isSwitchTask(maybeLastTask)) { + const innerSwitchEdges = joinEdgeForSwitch( + maybeLastTask, + invertedIndex, + joinTask, + ); + + // If we only have a switch statement with no tasks in it. then connect default to join + result = { + joinOn: result.joinOn.concat(innerSwitchEdges.joinOn), + northPorts: result.northPorts.concat(innerSwitchEdges.northPorts), + directPaths: result.directPaths, + }; + } else { + const forkLastTasksF = _isEmpty(maybeLastTask) + ? [] + : [maybeLastTask!]; + result = { + joinOn: result.joinOn.concat( + forkLastTasksF.map((lt: CommonTaskDef) => ({ + id: `edge_jj_${lt.taskReferenceName}-${joinTask.taskReferenceName}`, + from: lt.taskReferenceName, + fromPort: `${lt.taskReferenceName}-south-port`, + toPort: `${joinTask.taskReferenceName}-joinOnTask-${invertedIndex}-north-port`, + to: joinTask.taskReferenceName, + ...maybeEdgeData( + joinTask, + lt, + false, + isTaskNotInJoinOn(joinTask.joinOn, lt.taskReferenceName), + ), + })), + ), + northPorts: result.northPorts.concat( + northPort( + { id: `${joinNode.id}-joinOnTask-${invertedIndex}` }, + invertedIndex, + true, + ), + ), + directPaths: result.directPaths, + }; + } + } + } + } + + const nodes = currentNodes.concat({ + ...joinNode, + ports: joinNode.ports.concat(result.northPorts), + }); + + return { + nodes, + edges: result.directPaths.concat(result.joinOn), + }; +}; diff --git a/ui-next/src/components/flow/nodes/mapper/layout.js b/ui-next/src/components/flow/nodes/mapper/layout.js new file mode 100644 index 0000000000..2892f44ea7 --- /dev/null +++ b/ui-next/src/components/flow/nodes/mapper/layout.js @@ -0,0 +1,119 @@ +import theme from "../../theme"; +import { TaskType } from "types"; +import _isNil from "lodash/isNil"; + +export const BOTTOM_PORT_MARGIN = 10; +const SWITCH_SIZE_INCREMENTER = 60; +const MIN_AMOUNT_OF_SWITCH_PORTS = 4; +const ADD_FORK_ADDITINAL_HEIGHT = 40; + +const computeAdditionalWidth = (portsAmount) => + portsAmount > MIN_AMOUNT_OF_SWITCH_PORTS + ? (portsAmount - MIN_AMOUNT_OF_SWITCH_PORTS) * SWITCH_SIZE_INCREMENTER + : 0; + +export const taskToSize = (task) => { + const { type, executionData = null } = task; + switch (type) { + case TaskType.START: + case TaskType.TERMINAL: + return { + width: theme.nodeTypes.TERMINAL.width, + height: theme.nodeTypes.TERMINAL.height, + }; + case TaskType.SWITCH_JOIN: + return { + width: theme.nodeTypes.SWITCH_JOIN.width, + height: theme.nodeTypes.SWITCH_JOIN.height, + }; + case TaskType.JOIN: + case TaskType.FORK_JOIN: { + const { forkTasks = [] } = task; + return { + width: + theme.nodeTypes.FORK_JOIN.width + + computeAdditionalWidth(forkTasks.length), + height: _isNil(executionData) + ? theme.nodeTypes.FORK_JOIN.height + ADD_FORK_ADDITINAL_HEIGHT + : theme.nodeTypes.FORK_JOIN.height, + }; + } + case TaskType.DYNAMIC_JOIN: + case TaskType.TERMINATE: + return { + width: theme.nodeTypes.FORK_JOIN.width, + height: theme.nodeTypes.FORK_JOIN.height, + }; + case TaskType.HTTP: + case TaskType.HTTP_POLL: + case TaskType.START_WORKFLOW: + return { + width: theme.nodeTypes.HTTP.width, + height: theme.nodeTypes.HTTP.height, + }; + case TaskType.EVENT: + return { + width: theme.nodeTypes.EVENT.width, + height: theme.nodeTypes.EVENT.height, + }; + case TaskType.WAIT: + return { + width: theme.nodeTypes.WAIT.width, + height: task?.executionData?.status ? 100 : theme.nodeTypes.WAIT.height, + }; + case TaskType.INLINE: + case TaskType.JSON_JQ_TRANSFORM: + return { + width: theme.nodeTypes.JSON_JQ_TRANSFORM.width, + height: theme.nodeTypes.JSON_JQ_TRANSFORM.height, + }; + case TaskType.DO_WHILE: + return { + width: theme.nodeTypes.DO_WHILE.width, + height: theme.nodeTypes.DO_WHILE.height, + }; + case TaskType.KAFKA_PUBLISH: + return { + width: theme.nodeTypes.KAFKA_PUBLISH.width, + height: theme.nodeTypes.KAFKA_PUBLISH.height, + }; + case TaskType.DECISION: + case TaskType.SWITCH: { + const { decisionCases = {} } = task; + return { + width: + theme.nodeTypes.SWITCH.width + + computeAdditionalWidth(Object.keys(decisionCases).length + 1), + height: theme.nodeTypes.SWITCH.height, + }; + } + case TaskType.FORK_JOIN_DYNAMIC: + return { + width: theme.nodeTypes.FORK_JOIN_DYNAMIC.width, + height: theme.nodeTypes.FORK_JOIN_DYNAMIC.height, + }; + case TaskType.TASK_SUMMARY: { + const summaryValues = Object.keys( + task?.executionData?.summary?.taskCountByStatus || {}, + ).length; + + const newHeight = + summaryValues === 1 ? summaryValues * 68 : summaryValues * 48; + + return { + width: theme.nodeTypes.TASK_SUMMARY.width, + height: theme.nodeTypes.TASK_SUMMARY.height + newHeight, + }; + } + case "FORK_JOIN_COLLAPSED": + return { + width: theme.nodeTypes.FORK_JOIN_COLLAPSED.width, + height: theme.nodeTypes.FORK_JOIN_COLLAPSED.height, + }; + default: + return { + width: theme.nodeTypes.DEFAULT.width, + height: theme.nodeTypes.DEFAULT.height, + }; + } +}; diff --git a/ui-next/src/components/flow/nodes/mapper/ports.ts b/ui-next/src/components/flow/nodes/mapper/ports.ts new file mode 100644 index 0000000000..2bc7aaa4ea --- /dev/null +++ b/ui-next/src/components/flow/nodes/mapper/ports.ts @@ -0,0 +1,34 @@ +import { NodeData, PortData, PortSide } from "reaflow"; +export const PORT_SOUTH = "SOUTH" as PortSide; +export const PORT_NORTH = "NORTH" as PortSide; + +export type DiagramPort = PortData & { index?: number }; + +export const northPort = ( + node: NodeData, + index?: number, + hidden = false, +): DiagramPort => { + const id = `${node.id}-north-port`; + return { + id, + width: 2, + height: 2, + side: PORT_NORTH, + disabled: true, + hidden, + index, + }; +}; + +export const southPort = (node: NodeData, index?: number): DiagramPort => { + const id = `${node.id}-south-port`; + return { + id, + width: 2, + height: 2, + side: PORT_SOUTH, + disabled: true, + index, + }; +}; diff --git a/ui-next/src/components/flow/nodes/mapper/predicates.ts b/ui-next/src/components/flow/nodes/mapper/predicates.ts new file mode 100644 index 0000000000..e9b3fc6184 --- /dev/null +++ b/ui-next/src/components/flow/nodes/mapper/predicates.ts @@ -0,0 +1,47 @@ +import { + CommonTaskDef, + JoinTaskDef, + TaskType, + ForkJoinTaskDef, + ForkJoinDynamicDef, + DoWhileTaskDef, + TerminateTaskDef, + SubWorkflowTaskDef, + SwitchTaskDef, + ForkableTask, +} from "types"; + +export const isJoinTask = (task: CommonTaskDef): task is JoinTaskDef => + task?.type === TaskType.JOIN || task?.type === TaskType.EXCLUSIVE_JOIN; + +export const isForkJoinTask = (task: CommonTaskDef): task is ForkJoinTaskDef => + task?.type === TaskType.FORK_JOIN; + +export const isForkJoinDynamicTask = ( + task: CommonTaskDef, +): task is ForkJoinDynamicDef => task?.type === TaskType.FORK_JOIN_DYNAMIC; + +export const isDoWhileTask = (task: CommonTaskDef): task is DoWhileTaskDef => + task?.type === TaskType.DO_WHILE; + +export const isTerminateTask = ( + task: CommonTaskDef, +): task is TerminateTaskDef => task?.type === TaskType.TERMINATE; + +export const isSubWorkflowTask = ( + task: CommonTaskDef, +): task is SubWorkflowTaskDef => task?.type === TaskType.SUB_WORKFLOW; + +/** + * + * @param type Test if the task type will be processed as switch + * @returns + */ +export const isSwitchType = (type?: TaskType): boolean => + type != null && [TaskType.DECISION, TaskType.SWITCH].includes(type); + +export const isSwitchTask = (task?: CommonTaskDef): task is SwitchTaskDef => + isSwitchType(task?.type); + +export const isForkableTask = (task: CommonTaskDef): task is ForkableTask => + [TaskType.FORK_JOIN, TaskType.FORK_JOIN_DYNAMIC].includes(task.type); diff --git a/ui-next/src/components/flow/nodes/mapper/subWorkflow.test.js b/ui-next/src/components/flow/nodes/mapper/subWorkflow.test.js new file mode 100644 index 0000000000..75e18882e3 --- /dev/null +++ b/ui-next/src/components/flow/nodes/mapper/subWorkflow.test.js @@ -0,0 +1,118 @@ +import { + simpleDiagram, + subWorkflowWithinAFork, + wfWithWhileWithSubWorkflow, +} from "../../../../testData/diagramTests"; +import { tasksAsNodes } from "./core"; +import { processSubWorkflow } from "./subWorkflow"; + +const name = "testing_Errors"; + +describe("processSubWorkflow", () => { + const subWorkflowTask = { + name: "sub_workflow_x", + taskReferenceName: "wf4", + inputParameters: { + mod: "${task_1.output.mod}", + oddEven: "${task_1.output.oddEven}", + }, + type: "SUB_WORKFLOW", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + subWorkflowParam: { + name, + version: 1, + }, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }; + + it("Should include tasks provided by workflowFetcher to workflow", async () => { + const result = await processSubWorkflow( + subWorkflowTask, + [], + tasksAsNodes, + (__name, __version) => Promise.resolve(simpleDiagram), + ); + // Will include the node for subWorkflowTask + expect( + result.nodes.find(({ id }) => id === subWorkflowTask.taskReferenceName), + ).not.toBeUndefined(); + expect(result.nodes.length).toEqual(simpleDiagram.tasks.length + 1); + }); + + it("Should return empty if fetching failed", async () => { + const result = await processSubWorkflow( + subWorkflowTask, + [], + tasksAsNodes, + (__name, __version) => Promise.reject("Something Failed"), + ); + expect(result.nodes.length).toEqual(1); + expect(result.nodes[0].id).toEqual(subWorkflowTask.taskReferenceName); + }); + + it("Should not fetch if name provided is empty", async () => { + const result = await processSubWorkflow( + { ...subWorkflowTask, subWorkflowParam: { name: "" } }, + [], + tasksAsNodes, + (__name, __version) => Promise.resolve([{ rubish: "true" }]), + ); + expect(result.nodes.length).toEqual(1); + expect(result.nodes[0].id).toEqual(subWorkflowTask.taskReferenceName); + }); + + it("Should add a suffix to every id within the sub-workflow created nodes", async () => { + const { nodes, edges } = await processSubWorkflow( + subWorkflowTask, + [], + tasksAsNodes, + (__name, __version) => Promise.resolve({ ...simpleDiagram, name }), + ); + + const [subWorkflowNode, ...subWorkflowNodes] = nodes; + + expect(subWorkflowNode.id).toBe(subWorkflowTask.taskReferenceName); + expect(subWorkflowNodes.every(({ id }) => id.includes(name))).toBeTruthy(); + + expect( + subWorkflowNodes.every(({ ports }) => + ports.every(({ id }) => id.includes(name)), + ), + ).toBeTruthy(); + + expect( + edges.every(({ from, to }) => from.includes(name) && to.includes(name)), + ).toBeTruthy(); + + expect(edges.every(({ id }) => id.includes(name))).toBeTruthy(); + }); + + it("Should expand the sub-workflow if the workflow is within a WHILE", async () => { + const { nodes } = await tasksAsNodes(wfWithWhileWithSubWorkflow.tasks, { + expandSubWorkflow: true, + subWorkFlowFetcher: (__name, __version) => Promise.resolve(simpleDiagram), + }); + const expandedSubWorkflowNodes = nodes.filter( + ({ parent }) => parent === "sample_task_name_sub_workflow_ref", + ); + expect(expandedSubWorkflowNodes.length > 1).toBeTruthy(); + }); + + it("Should expand the sub-workflow if the workflow is within a FORK", async () => { + const { nodes } = await tasksAsNodes(subWorkflowWithinAFork.tasks, { + expandSubWorkflow: true, + subWorkFlowFetcher: (__name, __version) => Promise.resolve(simpleDiagram), + }); + const expandedSubWorkflowNodes = nodes.filter( + ({ parent }) => parent === "sample_task_name_sub_workflow_ref", + ); + expect(expandedSubWorkflowNodes.length > 1).toBeTruthy(); + }); +}); diff --git a/ui-next/src/components/flow/nodes/mapper/subWorkflow.ts b/ui-next/src/components/flow/nodes/mapper/subWorkflow.ts new file mode 100644 index 0000000000..4b46e7325a --- /dev/null +++ b/ui-next/src/components/flow/nodes/mapper/subWorkflow.ts @@ -0,0 +1,106 @@ +import _isUndefined from "lodash/isUndefined"; +import _isNil from "lodash/isNil"; +import _isEmpty from "lodash/isEmpty"; +import _flow from "lodash/flow"; +import { SubWorkflowTaskDef, Crumb, TaskDef } from "types"; +import { NodeData, EdgeData } from "reaflow"; +import { taskToNode } from "./common"; +import { logger } from "utils"; +import { SubWorkflowFunction } from "./types"; + +const reconfigureNodePorts = + (idSuffix: string) => + ({ ports, ...values }: NodeData) => ({ + ...values, + ports: _isUndefined(ports) + ? undefined + : ports.map((p) => ({ ...p, id: `${p.id}${idSuffix}`, hidden: true })), // Get rid of ports + }); + +const ifNotSetParentSetParent = (parent: string) => (t: NodeData) => + _isNil(t.parent) + ? { + ...t, + parent, + } + : t; + +const updateId = + (idSuffix: string) => + ({ id, ...rest }: NodeData) => ({ + ...rest, + id: `${id}${idSuffix}`, + ...(_isNil(rest.parent) ? {} : { parent: `${rest.parent}${idSuffix}` }), + }); + +const setReadOnly = (node: NodeData) => ({ + ...node, + ...{ data: { ...node.data, withinExpandedSubWorkflow: true } }, +}); + +const reconfigureEdgePorts = + (idSuffix: string) => + ({ fromPort, toPort, from, to, ...edgeProps }: EdgeData) => ({ + ...edgeProps, + to: `${to}${idSuffix}`, + from: `${from}${idSuffix}`, + fromPort: `${fromPort}${idSuffix}`, + toPort: `${toPort}${idSuffix}`, + }); + +export const processSubWorkflow = async ( + subWorkflowTask: SubWorkflowTaskDef, + crumbs: Crumb[], + taskWalkerFn: any, + subWorkflowFetcher: SubWorkflowFunction, +) => { + const randSuffix = Math.random().toString(36).substring(2, 7); + const { + subWorkflowParam: { name, version }, + taskReferenceName, + } = subWorkflowTask; + + const subWorkflowNode = [ + taskToNode(subWorkflowTask as unknown as TaskDef, crumbs), + ]; + if (!_isEmpty(name)) { + try { + const subWorkflow = await subWorkflowFetcher(name, version); + + const subWorkflowNodeEdges = await taskWalkerFn(subWorkflow.tasks, { + crumbContext: { + parent: subWorkflowTask.taskReferenceName, + }, + crumbs, + expandSubWorkflow: false, + readOnly: true, + }); + const idSuffix = `_swt_${name}_${randSuffix}`; + const nodePortsMapper = reconfigureNodePorts(idSuffix); + const parentSetter = ifNotSetParentSetParent(taskReferenceName); + const idUpdater = updateId(idSuffix); + const edgePortMapper = reconfigureEdgePorts(idSuffix); + + const wfNodesEdges = { + // TODO fix when using sdk + nodes: subWorkflowNode.concat( + subWorkflowNodeEdges.nodes.map( + _flow([nodePortsMapper, idUpdater, parentSetter, setReadOnly]), + ), + ), + edges: subWorkflowNodeEdges.edges.map( + _flow([nodePortsMapper, idUpdater, edgePortMapper, parentSetter]), + ), + crumbs: crumbs.concat(subWorkflowNodeEdges.crumbs), + }; + + return wfNodesEdges; + } catch (err) { + logger.error("Error when using subworkflow fetcher ", err); + } + } + return { + nodes: subWorkflowNode, + edges: [], + }; +}; diff --git a/ui-next/src/components/flow/nodes/mapper/switch.test.js b/ui-next/src/components/flow/nodes/mapper/switch.test.js new file mode 100644 index 0000000000..00dea95f60 --- /dev/null +++ b/ui-next/src/components/flow/nodes/mapper/switch.test.js @@ -0,0 +1,1156 @@ +import _dropRight from "lodash/dropRight"; +import _first from "lodash/first"; +import _last from "lodash/last"; +import _merge from "lodash/merge"; +import _update from "lodash/update"; +import { + decisionExecutionDataWithValidCase, + lonleySwitchTask, + switchExecutionDefaultByEvaluationResultNull, + switchTasksWithTerminationNodes, + switchTaskWithADecisionButNoTerminateTasks, + unConnectedSwitchTask, +} from "../../../../testData/diagramTests"; +import { extractTaskReferenceName, tasksAsNodes } from "./core"; +import { + createFakeNode, + decisionBranchesToNodesEdgesByCase, + drillForEndTasks, + processSwitchTasks, + switchFakeTaskEdges, + switchTaskToDecisionsToProcess, + switchTaskToFakeNodeId, + switchTaskToNode, + taskToSwitchNodesEdges, +} from "./switch"; + +const isNonSwitchPred = ({ type }) => type !== "SWITCH"; +const filterNonSwitch = (arr) => arr.filter(isNonSwitchPred); + +describe("taskToSwitchNodesEdges", () => { + it("Should return a switch task node with only connections to the pseudo task since the switch has not decisions", async () => { + const switchTaskNode = await taskToSwitchNodesEdges( + unConnectedSwitchTask, + [], + tasksAsNodes, + ); + expect(switchTaskNode.nodes.length).toBe(2); // switch task and pseudo task + expect(switchTaskNode.edges.length).toBe(1); // connection of default case to pseudo task + expect(switchTaskNode.everyTaskIsTerminate).toBe(false); + }); + it("Should return a node for switch a a node for pseudo switch a node for http and their connection edges. everyTaskIsTerminate should be false", async () => { + const switchTaskNode = await taskToSwitchNodesEdges( + switchTaskWithADecisionButNoTerminateTasks, + [], + tasksAsNodes, + ); + expect(switchTaskNode.nodes.length).toBe(3); // switch task and pseudo task and http task + + expect(switchTaskNode.edges).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + from: "sample_task_name_h64r7_ref", + to: "sample_task_name_yioskj_ref", + text: "some_case", + }), + expect.objectContaining({ + from: "sample_task_name_yioskj_ref", + to: "sample_task_name_h64r7_ref_switch_join", + }), + expect.objectContaining({ + from: "sample_task_name_h64r7_ref", + to: "sample_task_name_h64r7_ref_switch_join", + text: "defaultCase", + }), + ]), + ); + // expect(switchTaskNode.edges.length).toBe(2); // connection of default case to pseudo task + expect(switchTaskNode.everyTaskIsTerminate).toBe(false); + }); + it("Should connect to Terminate task and return everyTaskIsTerminate false. since defaultCase is empty", async () => { + const switchTaskOneTerminate = { + name: "switch_task_jgkrgj", + taskReferenceName: "switch_task_jgkrgj_ref", + inputParameters: { + switchCaseValue: "", + }, + type: "SWITCH", + decisionCases: { + new_case_pdiat: [ + { + name: "terminate_task_fhezy", + taskReferenceName: "terminate_task_fhezy_ref", + inputParameters: { + terminationStatus: "COMPLETED", + }, + type: "TERMINATE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + }; + const switchTaskNode = await taskToSwitchNodesEdges( + switchTaskOneTerminate, + [], + tasksAsNodes, + ); + expect(switchTaskNode.nodes.length).toBe(3); // switch task and pseudo task and http task + expect(switchTaskNode.edges.length).toBe(3); // connection of default case to pseudo task. Terminate task now connects so 3 + expect(switchTaskNode.everyTaskIsTerminate).toBe(false); + }); + it("Should connect to Terminate task and return everyTaskIsTerminate true. if every task ends in terminate", async () => { + const switchTaskOneTerminate = { + name: "switch_task_jgkrgj", + taskReferenceName: "switch_task_jgkrgj_ref", + inputParameters: { + switchCaseValue: "", + }, + type: "SWITCH", + defaultCase: [ + { + name: "other_name", + taskReferenceName: "other_task_reference", + inputParameters: { + terminationStatus: "COMPLETED", + }, + type: "TERMINATE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + decisionCases: { + new_case_pdiat: [ + { + name: "terminate_task_fhezy", + taskReferenceName: "terminate_task_fhezy_ref", + inputParameters: { + terminationStatus: "COMPLETED", + }, + type: "TERMINATE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + }; + const switchTaskNode = await taskToSwitchNodesEdges( + switchTaskOneTerminate, + [], + tasksAsNodes, + ); + expect(switchTaskNode.nodes.length).toBe(4); // switch task and both terminates, We are now connecting to terminate + expect(switchTaskNode.edges.length).toBe(4); // one connection for each terminate from switch + expect(switchTaskNode.everyTaskIsTerminate).toBe(true); + }); +}); + +describe("switchTaskToNode", () => { + const decisionKeys = [ + "emptyCase", + "education", + "property", + "business", + "defaultCase", + ]; + const switchTaskNode = switchTaskToNode(lonleySwitchTask, [], decisionKeys); + const fakeNodeForSwitch = createFakeNode(lonleySwitchTask, [], decisionKeys); + it("Should return a switch task with south ports and index specified", () => { + expect(switchTaskNode.id).toEqual(lonleySwitchTask.taskReferenceName); + expect(switchTaskNode.data.task).toEqual(lonleySwitchTask); + expect(switchTaskNode.ports).toEqual([ + { + id: "loan_type_[key=emptyCase]-south-port", + width: 2, + height: 2, + side: "SOUTH", + disabled: true, + index: 0, + }, + { + id: "loan_type_[key=education]-south-port", + width: 2, + height: 2, + side: "SOUTH", + disabled: true, + index: 1, + }, + { + id: "loan_type_[key=property]-south-port", + width: 2, + height: 2, + side: "SOUTH", + disabled: true, + index: 2, + }, + { + id: "loan_type_[key=business]-south-port", + width: 2, + height: 2, + side: "SOUTH", + disabled: true, + index: 3, + }, + { + id: "loan_type_[key=defaultCase]-south-port", + width: 2, + height: 2, + side: "SOUTH", + disabled: true, + index: 4, + }, + ]); + expect(fakeNodeForSwitch.ports).toEqual([ + { + id: "switch_fake_task_loan_type_switch_join-south-port", + side: "SOUTH", + disabled: true, + width: 2, + height: 2, + }, + { + id: "loan_type_switch_join-to-join-to=[key=emptyCase]-north-port", + width: 2, + height: 2, + side: "NORTH", + disabled: true, + hidden: true, + index: 4, + }, + { + id: "loan_type_switch_join-to-join-to=[key=education]-north-port", + width: 2, + height: 2, + side: "NORTH", + disabled: true, + hidden: true, + index: 3, + }, + { + id: "loan_type_switch_join-to-join-to=[key=property]-north-port", + width: 2, + height: 2, + side: "NORTH", + disabled: true, + hidden: true, + index: 2, + }, + { + id: "loan_type_switch_join-to-join-to=[key=business]-north-port", + width: 2, + height: 2, + side: "NORTH", + disabled: true, + hidden: true, + index: 1, + }, + { + id: "loan_type_switch_join-to-join-to=[key=defaultCase]-north-port", + width: 2, + height: 2, + side: "NORTH", + disabled: true, + hidden: true, + index: 0, + }, + ]); + }); +}); + +describe("drillForEndTasks", () => { + it("Should return an empty array, if switch branches end in a Terminal task TERMINATE or TERMINAL", async () => { + const unterminatedTasksConf = drillForEndTasks(tasksAsNodes); + const unterminatedTasks = await unterminatedTasksConf( + switchTasksWithTerminationNodes, + ); + + expect(filterNonSwitch(unterminatedTasks).length).toBe(0); + }); + it("Should return the task that was not terminated", async () => { + const switchTaskWithUnterminatedBranch = _update( + { ...switchTasksWithTerminationNodes }, + "decisionCases.education", + _dropRight, + ); + const unterminatedTasksConf = drillForEndTasks(tasksAsNodes); + const unterminatedTasks = await unterminatedTasksConf( + switchTaskWithUnterminatedBranch, + ); + expect(filterNonSwitch(unterminatedTasks).length).toBe(1); + expect(_first(filterNonSwitch(unterminatedTasks)).name).toBe( + "education_details", + ); + }); + it("Should return no task if all final tasks are terminated", async () => { + const switchTaskWithUnterminatedBranch = _update( + { ...switchTasksWithTerminationNodes }, + "decisionCases.education", + (arr) => { + return _dropRight(arr).concat({ + name: "finalcondition", + taskReferenceName: "finalCase", + inputParameters: { + finalCase: "${workflow.input.finalCase}", + }, + type: "SWITCH", + decisionCases: { + notify: [ + { + name: "integration_task_4", + taskReferenceName: "integration_task_4", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "terminate", + taskReferenceName: "terminate0", + inputParameters: { + terminationStatus: "COMPLETED", + workflowOutput: { result: "${task0.output}" }, + }, + type: "TERMINATE", + startDelay: 0, + optional: false, + }, + ], + }, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "finalCase", + }); + }, + ); + + const unterminatedTasksConf = drillForEndTasks(tasksAsNodes); + const unterminatedTasks = await unterminatedTasksConf( + switchTaskWithUnterminatedBranch, + ); + expect(filterNonSwitch(unterminatedTasks).length).toBe(0); + }); + + it("Should return empty if nested Switch has terminated tasks", async () => { + const switchTaskWithUnterminatedBranch = _update( + { ...switchTasksWithTerminationNodes }, + "decisionCases.education", + (arr) => { + return _dropRight(arr).concat({ + name: "finalcondition", + taskReferenceName: "finalCase", + inputParameters: { + finalCase: "${workflow.input.finalCase}", + }, + type: "SWITCH", + decisionCases: { + notify: [ + { + name: "integration_task_4", + taskReferenceName: "integration_task_4", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + }, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "finalCase", + }); + }, + ); + + const unterminatedTasksConf = drillForEndTasks(tasksAsNodes); + const unterminatedTasks = await unterminatedTasksConf( + switchTaskWithUnterminatedBranch, + ); + expect(filterNonSwitch(unterminatedTasks).length).toBe(1); + expect(_first(filterNonSwitch(unterminatedTasks)).name).toBe( + "integration_task_4", + ); + }); +}); + +describe("Switch", () => { + describe("switchTaskToDecisionsToProcess", () => { + it("Should return available tasks to process defaultPath", () => { + const swithTaskNoDefaults = { ...lonleySwitchTask, defaultCase: [] }; + const decisionBranches = + switchTaskToDecisionsToProcess(swithTaskNoDefaults); + const decisionKeys = Object.keys(decisionBranches); + expect(decisionKeys).toEqual( + expect.arrayContaining([ + "education", + "property", + "business", + "defaultCase", + ]), + ); + const lastKey = decisionKeys[decisionKeys.length - 1]; + expect(lastKey).toBe("defaultCase"); + }); + it("Should return available tasks to process defaultPath where default case is equal to cas", () => { + const decisionBranches = switchTaskToDecisionsToProcess(lonleySwitchTask); + const decisionKeys = Object.keys(decisionBranches); + expect(decisionKeys).toEqual( + expect.arrayContaining([ + "education", + "property", + "business", + "defaultCase", + ]), + ); + }); + it("Should return available tasks including defaultPath when default is not equal to an existing path", () => { + const swithTaskNoDefaults = { + ...lonleySwitchTask, + defaultCase: [ + { + name: "task_10", + taskReferenceName: "task_10_last", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + }; + const decisionBranches = + switchTaskToDecisionsToProcess(swithTaskNoDefaults); + const decisionKeys = Object.keys(decisionBranches); + expect(decisionKeys).toEqual( + expect.arrayContaining([ + "education", + "property", + "business", + "defaultCase", + ]), + ); + }); + }); + describe("decisionBranchesToNodesEdgesByCase", () => { + it("Should return tasks and edges for available decissions", async () => { + const educationExpectedId = extractTaskReferenceName( + lonleySwitchTask.decisionCases.education, + ); + const businessExpectedId = extractTaskReferenceName( + lonleySwitchTask.decisionCases.business, + ); + const propertyExpectedId = extractTaskReferenceName( + lonleySwitchTask.decisionCases.property, + ); + + const defaultCaseExpectedId = extractTaskReferenceName( + lonleySwitchTask.defaultCase, + ); + + const result = await decisionBranchesToNodesEdgesByCase( + lonleySwitchTask, + [], + tasksAsNodes, + ); + const extractNodeId = (name) => result[name].nodes.map(({ id }) => id); + const educationNodeNames = extractNodeId("education"); + const businessNodeNames = extractNodeId("business"); + const propertiesNodeNames = extractNodeId("property"); + const defaultCaseNodeNames = extractNodeId("defaultCase"); + + expect(educationNodeNames).toEqual( + expect.arrayContaining(educationExpectedId), + ); + expect(businessNodeNames).toEqual( + expect.arrayContaining(businessExpectedId), + ); + expect(propertiesNodeNames).toEqual( + expect.arrayContaining(propertyExpectedId), + ); + expect(propertyExpectedId).toEqual( + expect.arrayContaining(propertyExpectedId), + ); + expect(defaultCaseNodeNames).toEqual( + expect.arrayContaining(defaultCaseExpectedId), + ); + }); + }); + describe("processSwitchTasks", () => { + it("Should return al nodes in every single case, last node in every branch. undefined if the branch is empty. and decisionKey, order should be in the same order as the nodes", async () => { + const educationExpectedId = extractTaskReferenceName( + lonleySwitchTask.decisionCases.education, + ); + const businessExpectedId = extractTaskReferenceName( + lonleySwitchTask.decisionCases.business, + ); + const propertyExpectedId = extractTaskReferenceName( + lonleySwitchTask.decisionCases.property, + ); + + const defaultCaseExpectedId = extractTaskReferenceName( + lonleySwitchTask.defaultCase, + ); + + const { nodes, lastSwitchTasks, decisionKeys, lastSwitchNodes } = + await processSwitchTasks(lonleySwitchTask, [], tasksAsNodes); + + const nodesName = nodes.map(({ id }) => id); + // Every Node is covered + expect(nodesName).toEqual( + expect.arrayContaining( + educationExpectedId + .concat(businessExpectedId) + .concat(propertyExpectedId) + .concat(defaultCaseExpectedId), + ), + ); + + // Every last task is there + expect(lastSwitchTasks.map((task) => task?.taskReferenceName)).toEqual( + expect.arrayContaining([ + _last(lonleySwitchTask.decisionCases.education).taskReferenceName, + _last(lonleySwitchTask.decisionCases.business).taskReferenceName, + _last(lonleySwitchTask.decisionCases.property).taskReferenceName, + _last(lonleySwitchTask.defaultCase).taskReferenceName, + ]), + ); + + // The empty path should be shown as undefined + expect(lastSwitchNodes.some((a) => a === undefined)).toBe(true); + + //Every possible branch is there + expect(decisionKeys).toEqual( + expect.arrayContaining( + Object.keys(lonleySwitchTask.decisionCases).concat("defaultCase"), + ), + ); + + const undefinedNodeIdx = lastSwitchNodes.findIndex( + (a) => a === undefined, + ); + const emptyBranchDecsionIdx = decisionKeys.findIndex( + (k) => k === "emptyCase", + ); + // order of undefined node is the same as the emptyCase + expect(undefinedNodeIdx).not.toBe(-1); + expect(undefinedNodeIdx).toEqual(emptyBranchDecsionIdx); + }); + }); +}); + +describe("createFakeNode", () => { + const decisionCasesKeys = Object.keys(lonleySwitchTask.decisionCases).concat( + "defaultCase", + ); + const result = createFakeNode(lonleySwitchTask, [], decisionCasesKeys); + it("Should generate a fake node. with as many north ports as cases there is. and one SOUTH port", () => { + const northPorts = result.ports.filter(({ side }) => side === "NORTH"); + expect(northPorts.length).toBe(decisionCasesKeys.length); + // Should have the same order + expect( + decisionCasesKeys.every( + (k, idx) => + northPorts[idx].id === + `${switchTaskToFakeNodeId( + lonleySwitchTask, + )}-to-join-to=[key=${k}]-north-port`, + ), + ).toBeTruthy(); + }); +}); + +describe("switchFakeTaskEdges", () => { + const switchCreatedNode = { + id: "pin_validation", + text: "pin_validation", + ports: [ + { + id: "pin_validation_[key=]-south-port", + width: 2, + height: 2, + side: "SOUTH", + disabled: true, + index: 0, + }, + { + id: "pin_validation_[key=CASE-2]-south-port", + width: 2, + height: 2, + side: "SOUTH", + disabled: true, + index: 1, + }, + { + id: "pin_validation_[key=CASE-1]-south-port", + width: 2, + height: 2, + side: "SOUTH", + disabled: true, + index: 2, + }, + { + id: "pin_validation_[key=defaultCase]-south-port", + width: 2, + height: 2, + side: "SOUTH", + disabled: true, + index: 3, + }, + ], + data: { + task: { + name: "pin_validation", + taskReferenceName: "pin_validation", + inputParameters: { + case: "${workflow.input.case}", + }, + type: "SWITCH", + decisionCases: { + "": [], + "CASE-2": [], + "CASE-1": [], + }, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: "((\n function () {\n return $.case;\n }\n))();", + onStateChange: {}, + executionData: { + status: "COMPLETED", + executed: true, + attempts: 1, + outputData: { + evaluationResult: ["null"], + selectedCase: "null", + }, + }, + }, + crumbs: [ + { + parent: null, + ref: "pin_validation", + refIdx: 0, + type: "SWITCH", + }, + ], + status: "COMPLETED", + executed: true, + attempts: 1, + outputData: { + evaluationResult: ["null"], + selectedCase: "null", + }, + }, + width: 450, + height: 200, + }; + const fakeNode = { + id: "pin_validation_switch_join", + data: { + task: { + name: "pin_validation", + taskReferenceName: "pin_validation", + inputParameters: { + case: "${workflow.input.case}", + }, + type: "SWITCH_JOIN", + decisionCases: { + "": [], + "CASE-2": [], + "CASE-1": [], + }, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: "((\n function () {\n return $.case;\n }\n))();", + onStateChange: {}, + executionData: { + status: "COMPLETED", + executed: true, + attempts: 1, + outputData: { + evaluationResult: ["null"], + selectedCase: "null", + }, + }, + }, + crumbs: [ + { + parent: null, + ref: "pin_validation", + refIdx: 0, + type: "SWITCH", + }, + ], + originalTask: { + name: "pin_validation", + taskReferenceName: "pin_validation", + inputParameters: { + case: "${workflow.input.case}", + }, + type: "SWITCH", + decisionCases: { + "": [], + "CASE-2": [], + "CASE-1": [], + }, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: "((\n function () {\n return $.case;\n }\n))();", + onStateChange: {}, + executionData: { + status: "COMPLETED", + executed: true, + attempts: 1, + outputData: { + evaluationResult: ["null"], + selectedCase: "null", + }, + }, + }, + }, + ports: [ + { + id: "switch_fake_task_pin_validation_switch_join-south-port", + side: "SOUTH", + disabled: true, + width: 2, + height: 2, + }, + { + id: "pin_validation_switch_join-to-join-to=[key=]-north-port", + width: 2, + height: 2, + side: "NORTH", + disabled: true, + hidden: true, + index: 3, + }, + { + id: "pin_validation_switch_join-to-join-to=[key=CASE-2]-north-port", + width: 2, + height: 2, + side: "NORTH", + disabled: true, + hidden: true, + index: 2, + }, + { + id: "pin_validation_switch_join-to-join-to=[key=CASE-1]-north-port", + width: 2, + height: 2, + side: "NORTH", + disabled: true, + hidden: true, + index: 1, + }, + { + id: "pin_validation_switch_join-to-join-to=[key=defaultCase]-north-port", + width: 2, + height: 2, + side: "NORTH", + disabled: true, + hidden: true, + index: 0, + }, + ], + text: "pin_validation_switch_join", + width: 350, + height: 55, + }; + + it("Should paint the defaultCase as green (COMPLETED) - when empty decision case is present and selectedCase is null", () => { + const decisionKeys = ["", "CASE-2", "CASE-1", "defaultCase"]; + const result = switchFakeTaskEdges( + [undefined, undefined, undefined, undefined], + [undefined, undefined, undefined, undefined], + switchCreatedNode, + decisionKeys, + fakeNode, + null, + ); + + const result1 = [ + { + id: "edge_dp__fake_pin_validation_-direct", + from: "pin_validation", + fromPort: "pin_validation_[key=]-south-port", + toPort: "pin_validation_switch_join-to-join-to=[key=]-north-port", + to: "pin_validation_switch_join", + text: "", + }, + { + id: "edge_dp__fake_pin_validation_CASE-2-direct", + from: "pin_validation", + fromPort: "pin_validation_[key=CASE-2]-south-port", + toPort: "pin_validation_switch_join-to-join-to=[key=CASE-2]-north-port", + to: "pin_validation_switch_join", + text: "CASE-2", + }, + { + id: "edge_dp__fake_pin_validation_CASE-1-direct", + from: "pin_validation", + fromPort: "pin_validation_[key=CASE-1]-south-port", + toPort: "pin_validation_switch_join-to-join-to=[key=CASE-1]-north-port", + to: "pin_validation_switch_join", + text: "CASE-1", + }, + { + id: "edge_dp__fake_pin_validation_defaultCase-direct", + from: "pin_validation", + fromPort: "pin_validation_[key=defaultCase]-south-port", + toPort: + "pin_validation_switch_join-to-join-to=[key=defaultCase]-north-port", + to: "pin_validation_switch_join", + text: "defaultCase", + data: { status: "COMPLETED" }, + }, + ]; + expect(result).toEqual(result1); + }); + it("Should paint green default if selected case is not in the decision keys", async () => { + const switchTaskNode = await taskToSwitchNodesEdges( + switchExecutionDefaultByEvaluationResultNull, + [], + tasksAsNodes, + ); + expect(switchTaskNode.edges).toEqual([ + { + id: "edge_pin_validation-http_ref", + from: "pin_validation", + to: "http_ref", + fromPort: "pin_validation_[key=defaultCase]-south-port", + toPort: "pin_validation_[key=defaultCase]-south-port-to", + text: "defaultCase", + data: { + status: "COMPLETED", + unreachableEdge: false, + action: "ADD_TASK_ABOVE", + }, + }, + { + id: "edge_dp__fake_pin_validation_-direct", + from: "pin_validation", + fromPort: "pin_validation_[key=]-south-port", + toPort: "pin_validation_switch_join-to-join-to=[key=]-north-port", + to: "pin_validation_switch_join", + text: "", + }, + { + id: "edge_dp__fake_pin_validation_CASE-2-direct", + from: "pin_validation", + fromPort: "pin_validation_[key=CASE-2]-south-port", + toPort: "pin_validation_switch_join-to-join-to=[key=CASE-2]-north-port", + to: "pin_validation_switch_join", + text: "CASE-2", + }, + { + id: "edge_dp__fake_pin_validation_CASE-1-direct", + from: "pin_validation", + fromPort: "pin_validation_[key=CASE-1]-south-port", + toPort: "pin_validation_switch_join-to-join-to=[key=CASE-1]-north-port", + to: "pin_validation_switch_join", + text: "CASE-1", + }, + { + id: "edge_dp__fake_pin_validation_http_ref", + from: "http_ref", + to: "pin_validation_switch_join", + toPort: + "pin_validation_switch_join-to-join-to=[key=defaultCase]-north-port", + fromPort: "http_ref-south-port", + data: { + status: "COMPLETED", + }, + }, + ]); // switch task and pseudo task and http task + + // Test DECISION task with empty caseOutput + const decisionTaskWithEmptyCase = { + ...decisionExecutionDataWithValidCase, + type: "DECISION", + executionData: { + status: "COMPLETED", + executed: true, + attempts: 1, + outputData: { + evaluationResult: [], + caseOutput: "", + }, + }, + }; + + const decisionTaskNodeEmptyCase = await taskToSwitchNodesEdges( + decisionTaskWithEmptyCase, + [], + tasksAsNodes, + ); + + // Verify defaultCase is marked as completed when caseOutput is empty + const decisionDefaultCaseEdge = decisionTaskNodeEmptyCase.edges.find( + (edge) => edge.text === "defaultCase", + ); + expect(decisionDefaultCaseEdge?.data?.status).toBe("COMPLETED"); + + // Test DECISION task with invalid caseOutput + const decisionTaskWithInvalidCase = { + ...decisionExecutionDataWithValidCase, + type: "DECISION", + executionData: { + status: "COMPLETED", + executed: true, + attempts: 1, + outputData: { + evaluationResult: ["INVALID_CASE"], + caseOutput: "INVALID_CASE", + }, + }, + }; + + const decisionTaskNodeInvalidCase = await taskToSwitchNodesEdges( + decisionTaskWithInvalidCase, + [], + tasksAsNodes, + ); + + // Verify defaultCase is marked as completed when caseOutput is not in decision keys + const decisionDefaultCaseEdgeInvalid = + decisionTaskNodeInvalidCase.edges.find( + (edge) => edge.text === "defaultCase", + ); + expect(decisionDefaultCaseEdgeInvalid?.data?.status).toBe("COMPLETED"); + + // Test DECISION task with valid caseOutput matching a decision key + const decisionTaskWithValidCase = { + ...decisionExecutionDataWithValidCase, + type: "DECISION", + executionData: { + status: "COMPLETED", + executed: true, + attempts: 1, + outputData: { + evaluationResult: ["MEDIUM"], + caseOutput: "MEDIUM", + }, + }, + }; + + const decisionTaskNodeValidCase = await taskToSwitchNodesEdges( + decisionTaskWithValidCase, + [], + tasksAsNodes, + ); + + // Verify the matching case edge is marked as completed + const decisionCase1Edge = decisionTaskNodeValidCase.edges.find( + (edge) => edge.text === "MEDIUM", + ); + expect(decisionCase1Edge?.data?.status).toBe("COMPLETED"); + + // Verify defaultCase is NOT marked as completed when a valid case is selected + const decisionDefaultCaseEdgeValid = decisionTaskNodeValidCase.edges.find( + (edge) => edge.text === "defaultCase", + ); + expect(decisionDefaultCaseEdgeValid?.data?.status).toBeUndefined(); + + const switchTaskNodeCase1 = await taskToSwitchNodesEdges( + _merge({}, switchExecutionDefaultByEvaluationResultNull, { + executionData: { + status: "COMPLETED", + executed: true, + attempts: 1, + outputData: { + evaluationResult: ["CASE-1"], + selectedCase: "CASE-1", + }, + }, + }), + [], + tasksAsNodes, + ); + expect(switchTaskNodeCase1.edges).toEqual([ + { + id: "edge_pin_validation-http_ref", + from: "pin_validation", + to: "http_ref", + fromPort: "pin_validation_[key=defaultCase]-south-port", + toPort: "pin_validation_[key=defaultCase]-south-port-to", + text: "defaultCase", + data: { + action: "ADD_TASK_ABOVE", + }, + }, + { + id: "edge_dp__fake_pin_validation_-direct", + from: "pin_validation", + fromPort: "pin_validation_[key=]-south-port", + toPort: "pin_validation_switch_join-to-join-to=[key=]-north-port", + to: "pin_validation_switch_join", + text: "", + }, + { + id: "edge_dp__fake_pin_validation_CASE-2-direct", + from: "pin_validation", + fromPort: "pin_validation_[key=CASE-2]-south-port", + toPort: "pin_validation_switch_join-to-join-to=[key=CASE-2]-north-port", + to: "pin_validation_switch_join", + text: "CASE-2", + }, + { + id: "edge_dp__fake_pin_validation_CASE-1-direct", + from: "pin_validation", + fromPort: "pin_validation_[key=CASE-1]-south-port", + toPort: "pin_validation_switch_join-to-join-to=[key=CASE-1]-north-port", + to: "pin_validation_switch_join", + text: "CASE-1", + data: { + status: "COMPLETED", + }, + }, + { + id: "edge_dp__fake_pin_validation_http_ref", + from: "http_ref", + to: "pin_validation_switch_join", + toPort: + "pin_validation_switch_join-to-join-to=[key=defaultCase]-north-port", + fromPort: "http_ref-south-port", + data: { + status: "COMPLETED", + }, + }, + ]); + }); + it("should mark edge as unreachable when connecting from TERMINATE task", async () => { + const switchWithTerminate = { + name: "switch_with_terminate", + taskReferenceName: "switch_terminate_ref", + type: "SWITCH", + decisionCases: { + case1: [ + { + name: "terminate_task", + taskReferenceName: "terminate_ref", + type: "TERMINATE", + inputParameters: { + terminationStatus: "COMPLETED", + }, + }, + ], + }, + defaultCase: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + }; + + const { edges } = await taskToSwitchNodesEdges( + switchWithTerminate, + [], + async (tasks) => ({ + nodes: tasks.map((task) => ({ + id: task.taskReferenceName, + data: { task }, + ports: [], + })), + edges: [], + previousTask: tasks[tasks.length - 1], + previousTaskAllowsConnection: false, + }), + ); + + const terminateEdge = edges.find( + (edge) => + edge.from === "terminate_ref" && + edge.to === "switch_terminate_ref_switch_join", + ); + + expect(terminateEdge).toEqual( + expect.objectContaining({ + id: "edge_dp__fake_switch_terminate_ref_terminate_ref", + from: "terminate_ref", + to: "switch_terminate_ref_switch_join", + toPort: + "switch_terminate_ref_switch_join-to-join-to=[key=case1]-north-port", + data: { unreachableEdge: true }, + }), + ); + }); +}); diff --git a/ui-next/src/components/flow/nodes/mapper/switch.ts b/ui-next/src/components/flow/nodes/mapper/switch.ts new file mode 100644 index 0000000000..60ada9d070 --- /dev/null +++ b/ui-next/src/components/flow/nodes/mapper/switch.ts @@ -0,0 +1,506 @@ +import _isEmpty from "lodash/isEmpty"; +import _head from "lodash/head"; +import _pick from "lodash/pick"; +import _findLast from "lodash/findLast"; +import { ADD_TASK_ABOVE } from "pages/definition/state/taskModifier/constants"; +import { + extractExecutionDataOrEmpty, + taskHasCompleted, + edgeIdMapper, + completedTaskStatusData, +} from "./common"; +import { northPort, southPort } from "./ports"; +import { taskToSize } from "./layout"; +import { TaskType, SwitchTaskDef, Crumb, TaskDef, CommonTaskDef } from "types"; +import { NodeData, EdgeData, PortData, PortSide } from "reaflow"; +import { NodeTaskData, NodesAndEdges } from "./types"; +import { isSwitchType, isSwitchTask } from "./predicates"; + +type DecisionBranches = { + defaultCase: CommonTaskDef[]; + [k: string]: CommonTaskDef[]; +}; + +/** + * Takes a Switch returns an object with decisionCases and defaultCase + * *NOTE* defaulCase is added last so that when turning tu entries defaultCase is last + * @param switchTask + * @returns + */ +export const switchTaskToDecisionsToProcess = ( + switchTask: SwitchTaskDef, +): DecisionBranches => { + const { decisionCases, defaultCase = [] } = switchTask; + + const decisionBranches = { + ...decisionCases, + defaultCase, + }; + return decisionBranches; +}; + +type NodesEdgesCrumbsPreviousTask = NodesAndEdges & { + crumbs: Crumb[]; + previousTask: TaskDef; + previousTaskAllowsConnection: boolean; +}; + +/** + * Takes decisionBranches the switch task{taskReferenceName} initial crumbs + * and a taskWalker. will return nodes,edges,crumbs,previous task by branch + * @param switchTask + * @param crumbs + * @param taskWalkerFn + * @returns + */ +export const decisionBranchesToNodesEdgesByCase = async ( + switchTask: SwitchTaskDef, + crumbs: Crumb[], + taskWalkerFn: any, +): Promise<{ [k: string]: NodesEdgesCrumbsPreviousTask }> => { + const decisionBranches = switchTaskToDecisionsToProcess(switchTask); + + const decisionBranchEntries = Object.entries(decisionBranches); + let acc = {}; + for (const [, [k, innerTasks]] of decisionBranchEntries.entries()) { + acc = { + ...acc, + [k]: _pick( + await taskWalkerFn(innerTasks, { + crumbContext: { + parent: switchTask?.taskReferenceName, + decisionBranch: k, + }, + crumbs, + }), + [ + "nodes", + "edges", + "crumbs", + "previousTask", + "previousTaskAllowsConnection", + ], + ), + }; + } + return acc; +}; + +export type ProcessedSwitchTask = CommonTaskDef & { + allowsTaskConnection?: boolean; +}; + +type SwitchTaskNodesEdgesEndingTasksDecisionKeysEndingNodes = NodesAndEdges & { + decisionKeys: string[]; + lastSwitchTasks: Array; + lastSwitchNodes: Array; +}; + +const switchMaybeEdgeData = (switchTask: SwitchTaskDef) => (path: string) => { + const outputData = switchTask?.executionData?.outputData; + + if (!outputData) return {}; // Not an execution or not executed yet + const decisionCases = Object.keys(switchTask?.decisionCases || []); + const selectedCase = + switchTask?.type === TaskType.SWITCH + ? outputData?.selectedCase + : outputData?.caseOutput?.toString(); + const hasPath = decisionCases.includes(path); + const hasPathAndPathWasSelected = hasPath && path === selectedCase; + const caseIsDefaultCase = + !hasPath && path === "defaultCase" && !decisionCases.includes(selectedCase); + return hasPathAndPathWasSelected || // selected case is a valid path + caseIsDefaultCase + ? completedTaskStatusData(false) + : {}; +}; + +/** + * Returns every node that can be travered from the switchTask, every edge connected, every decision key + * The last task of every branch. and the last switch node. will insert undefined if node is empty + * so the order matches the decisionKeys + * + * @param switchTask + * @param crumbs + * @param taskWalkerFn + * @returns + */ +export const processSwitchTasks = async ( + switchTask: SwitchTaskDef, + crumbs: Crumb[], + taskWalkerFn: any, +): Promise => { + const decisionBranchesAsNodeEdges = await decisionBranchesToNodesEdgesByCase( + switchTask, + crumbs, + taskWalkerFn, + ); + const decisionEntries = Object.entries(decisionBranchesAsNodeEdges); + const maybeDataForSwitchPath = switchMaybeEdgeData(switchTask); + + return decisionEntries.reduce( + ( + acc: SwitchTaskNodesEdgesEndingTasksDecisionKeysEndingNodes, + [ + decisionKey, + { nodes, edges, previousTask, previousTaskAllowsConnection }, + ], + ) => { + let switchTaskConnectingEdge: EdgeData[] = []; + if (!_isEmpty(nodes)) { + // Move this to a different function + const { id: firstNodeId, data: firstNodeData } = _head(nodes)!; + const firstTask = firstNodeData!.task; + const edgeId: string = edgeIdMapper( + switchTask as CommonTaskDef, + firstTask, + ) as string; + switchTaskConnectingEdge = [ + { + id: edgeId, + from: switchTask.taskReferenceName, + to: firstNodeId, + fromPort: `${switchTask.taskReferenceName}_[key=${decisionKey}]-south-port`, + toPort: `${switchTask.taskReferenceName}_[key=${decisionKey}]-south-port-to`, + text: decisionKey, + data: { + ...maybeDataForSwitchPath(decisionKey), + action: ADD_TASK_ABOVE, + }, + }, + ]; + } + return { + edges: acc.edges.concat(switchTaskConnectingEdge).concat(edges), + nodes: acc.nodes.concat(nodes), + lastSwitchTasks: acc.lastSwitchTasks.concat( + previousTask != null + ? { + ...previousTask, + allowsTaskConnection: previousTaskAllowsConnection, + } + : undefined, // if empty return undefined this is intended. order is important + ), // This tasks should not get into node-data + decisionKeys: acc.decisionKeys.concat(decisionKey), + lastSwitchNodes: acc.lastSwitchNodes.concat( + _findLast(nodes, ({ id }) => id === previousTask?.taskReferenceName), + ), // if nodes is empty. this will yield undefined. and its ok its a desired effect + }; + }, + { + nodes: [], + edges: [], + lastSwitchTasks: [], + decisionKeys: [], + lastSwitchNodes: [], + }, + ); +}; + +export const switchTaskToNode = ( + task: SwitchTaskDef, + crumbs: Crumb[], + decisionKeys: string[], +): NodeData> => { + const { taskReferenceName, name } = task; + + const ports = decisionKeys.map((k, idx) => + southPort({ id: `${taskReferenceName}_[key=${k}]` }, idx), + ); + const switchSize = taskToSize(task); + const node = { + id: taskReferenceName, + text: name, + ports, + data: { + task, + crumbs, + ...extractExecutionDataOrEmpty(task), + }, + ...switchSize, + }; + return node; +}; + +type SwitchTaskDriller = ( + task: SwitchTaskDef, + endLeafTasks: CommonTaskDef[], +) => Promise; + +/** + * @deprecated This function made sense when no switch-join. + * Returns a function that takes a task. Will look for non terminated tasks + * used to identify missing connection edges + * @param {*} tasksAsNodes + * @returns + */ +export const drillForEndTasks = (tasksAsNodes: any): SwitchTaskDriller => { + const inner: SwitchTaskDriller = async ( + task: SwitchTaskDef, + endLeafTasks: ProcessedSwitchTask[] = [], + ) => { + const { lastSwitchTasks } = await processSwitchTasks( + task, + [], + tasksAsNodes, + ); + let acc: ProcessedSwitchTask[] = []; + for (const [, endTask] of lastSwitchTasks.entries()) { + if (isSwitchTask(endTask)) { + acc = acc + // .concat(_isEmpty(endTask.defaultCase) ? endTask : []) + .concat( + await inner(endTask as SwitchTaskDef, []), + ) as ProcessedSwitchTask[]; + } else if ( + endTask?.type === TaskType.TERMINATE || + endTask?.type === TaskType.TERMINAL + ) { + // TODO + } else { + acc = acc.concat( + endTask === undefined ? [] : endTask, + ) as ProcessedSwitchTask[]; + } + } + return endLeafTasks.concat(acc); + }; + return inner; +}; + +type SwitchTaskReferenceNonTerminatedReference = { + switchTr: CommonTaskDef[]; + nonTerminatedTr: CommonTaskDef[]; +}; + +export const nonTerminatedTasksGroupedAsTaskReferenceNameByType = ( + nonTerminatedTasks: TaskDef[], +) => + nonTerminatedTasks.reduce( + ( + { switchTr, nonTerminatedTr }: SwitchTaskReferenceNonTerminatedReference, + task, + ) => + isSwitchType(task.type) + ? { switchTr: switchTr.concat(task), nonTerminatedTr } + : { + switchTr, + nonTerminatedTr: nonTerminatedTr.concat(task), + }, + { switchTr: [], nonTerminatedTr: [] }, + ); + +export const switchTaskToFakeNodeId = ({ taskReferenceName }: SwitchTaskDef) => + `${taskReferenceName}_switch_join`; + +export const switchFakeTaskIDSouthPortId = (fakeTaskId: string) => + `switch_fake_task_${fakeTaskId}-south-port`; + +export const createFakeNode = ( + switchCaseTask: SwitchTaskDef, + crumbs: Crumb[], + decisionCasesKeys: string[], +): NodeData => { + const id = switchTaskToFakeNodeId(switchCaseTask); + const decisionCasesPorts: PortData[] = decisionCasesKeys.map((pk, idx) => + northPort( + { id: `${id}-to-join-to=[key=${pk}]` }, + (decisionCasesKeys?.length || 1) - 1 - idx, + true, + ), + ); + return { + id, + data: { + task: { + ...switchCaseTask, + type: "SWITCH_JOIN", + }, + crumbs, + originalTask: switchCaseTask, + }, + ports: [ + { + id: switchFakeTaskIDSouthPortId(id), + side: "SOUTH", + disabled: true, + width: 2, + height: 2, + } as PortData, + ].concat(decisionCasesPorts), + text: `${switchCaseTask.taskReferenceName}_switch_join`, + ...taskToSize({ type: TaskType.SWITCH_JOIN }), + }; +}; + +export const lastNodeToFakeTaskEdge = ( + lastNode: NodeData, + switchNode: NodeData, + fakeNodeId: string, + decisionBranch: string, +): EdgeData => { + const lastNodeTask = lastNode.data?.task; + const switchNodeTask = switchNode.data?.task; + const switchNodeHasCompleted = taskHasCompleted(switchNodeTask); + const maybeData = + switchNodeHasCompleted && taskHasCompleted(lastNodeTask) + ? { data: { status: lastNode?.data?.status } } + : {}; + switch (lastNodeTask?.type) { + case TaskType.DECISION: + case TaskType.SWITCH: { + // If last task is a switch we need to connect the pseudo task with the switch + const lastSwitchTaskFakeNodeId = switchTaskToFakeNodeId( + lastNodeTask as SwitchTaskDef, + ); + return { + id: `edge_dp__fake_${switchNode.id}_${lastNode.id}`, + from: lastSwitchTaskFakeNodeId, + fromPort: switchFakeTaskIDSouthPortId(lastSwitchTaskFakeNodeId), + toPort: `${fakeNodeId}-to-join-to=[key=${decisionBranch}]-north-port`, + to: fakeNodeId, + ...maybeData, + }; + } + case TaskType.TERMINATE: { + return { + id: `edge_dp__fake_${switchNode.id}_${lastNode.id}`, + from: lastNode.id, + to: fakeNodeId, + toPort: `${fakeNodeId}-to-join-to=[key=${decisionBranch}]-north-port`, + data: { unreachableEdge: true }, + }; + } + default: { + const maybeSouthPort = lastNode?.ports?.find( + (p) => p.side === ("SOUTH" as PortSide), + ); + + const portData = + maybeSouthPort !== undefined + ? { + fromPort: maybeSouthPort.id, + } + : {}; + + return { + id: `edge_dp__fake_${switchNode.id}_${lastNode.id}`, + from: lastNode.id, + to: fakeNodeId, + toPort: `${fakeNodeId}-to-join-to=[key=${decisionBranch}]-north-port`, + ...portData, + ...maybeData, + }; + } + } +}; + +export const switchFakeTaskEdges = ( + switchLastNodes: Array, + lastSwitchTasks: Array, + switchCreatedNode: NodeData, + decisionKeys: string[], + fakeNode: NodeData, + selectedCase?: string, +): EdgeData[] => { + return decisionKeys.reduce((acc: EdgeData[], k, idx) => { + const markPathAsCompleted = + k && + ((taskHasCompleted(switchCreatedNode?.data?.task) && + _isEmpty(selectedCase) && + k === "defaultCase") || + (taskHasCompleted(switchCreatedNode?.data?.task) && + !decisionKeys.includes(selectedCase!) && + k === "defaultCase") || + k === selectedCase); + if (switchLastNodes[idx] != null && lastSwitchTasks[idx] != null) { + return acc.concat( + lastNodeToFakeTaskEdge( + switchLastNodes[idx]!, + switchCreatedNode, + fakeNode.id, + k, + ), + ); + } + return acc.concat({ + // if no node + id: `edge_dp__fake_${switchCreatedNode.id}_${k}-direct`, + from: switchCreatedNode.id, + fromPort: `${switchCreatedNode.id}_[key=${k}]-south-port`, + toPort: `${fakeNode.id}-to-join-to=[key=${k}]-north-port`, + to: fakeNode.id, + text: k, + ...(markPathAsCompleted ? { data: { status: "COMPLETED" } } : {}), + }); + }, []); +}; + +const isEveryPathInSwitchTerminated = ( + switchTaskResult: SwitchTaskNodesEdgesEndingTasksDecisionKeysEndingNodes, +): boolean => { + const { lastSwitchTasks, decisionKeys } = switchTaskResult; + + return ( + !_isEmpty(lastSwitchTasks) && + decisionKeys.length === lastSwitchTasks.length && + lastSwitchTasks.every((task) => { + return task?.allowsTaskConnection === false; + }) + ); +}; + +export const taskToSwitchNodesEdges = async ( + currentTask: SwitchTaskDef, + crumbs: Crumb[], + taskWalkerFn: any, +): Promise => { + const switchResult = await processSwitchTasks( + currentTask, + crumbs, + taskWalkerFn, + ); + + const { + nodes: switchInnerNodes, + edges: switchInnerEdges, + decisionKeys, + lastSwitchNodes, + lastSwitchTasks, + } = switchResult; + + const switchNode = switchTaskToNode(currentTask, crumbs, decisionKeys); + + const everyTaskIsTerminate = isEveryPathInSwitchTerminated(switchResult); + + let additionalNodes: NodeData[] = []; + let additionalEdges: EdgeData[] = []; + + const fakeNode = createFakeNode(currentTask, crumbs, decisionKeys); + const selectedCase = + currentTask?.type === TaskType.SWITCH + ? currentTask?.executionData?.outputData?.selectedCase + : currentTask?.executionData?.outputData?.caseOutput?.toString(); + const fakeEdges: EdgeData[] = switchFakeTaskEdges( + lastSwitchNodes, + lastSwitchTasks, + switchNode, + decisionKeys, + fakeNode, + selectedCase, + ); + additionalNodes = [fakeNode]; + additionalEdges = fakeEdges; + + const switchNodeArray: NodeData[] = [switchNode]; + + // Only needed in edit mode. not in execution mode + return { + nodes: switchNodeArray.concat(switchInnerNodes).concat(additionalNodes), + edges: switchInnerEdges.concat(additionalEdges), + everyTaskIsTerminate, + }; +}; + +export const isSwitchPathEmpty = (portId: string, currentTask: SwitchTaskDef) => + portId && currentTask && currentTask.decisionCases?.[portId]?.length === 0; diff --git a/ui-next/src/components/flow/nodes/mapper/terminal.ts b/ui-next/src/components/flow/nodes/mapper/terminal.ts new file mode 100644 index 0000000000..07f1f4a43e --- /dev/null +++ b/ui-next/src/components/flow/nodes/mapper/terminal.ts @@ -0,0 +1,89 @@ +import { taskToNode } from "./common"; +import { + TaskType, + CommonTaskDef, + TaskStatus, + WorkflowExecutionStatus, +} from "types"; +import { NodeData, EdgeData } from "reaflow"; +import { edgeMapper } from "./edgeMapper"; +import _isEmpty from "lodash/isEmpty"; + +export const START_TASK_FAKE_TASK_REFERENCE_NAME = "start"; +export const END_TASK_FAKE_TASK_REFERENCE_NAME = "end"; + +type NodesAndEdges = { + nodes: NodeData[]; + edges: EdgeData[]; +}; + +const wfExecutionStatusToTaskStatus = ( + wfExecutionStatus: WorkflowExecutionStatus, +) => { + switch (wfExecutionStatus) { + case WorkflowExecutionStatus.COMPLETED: + return TaskStatus.COMPLETED; + default: + return TaskStatus.PENDING; + } +}; + +const endPseudoTask = ( + executionStatus: WorkflowExecutionStatus, +): CommonTaskDef => ({ + name: "end", + taskReferenceName: END_TASK_FAKE_TASK_REFERENCE_NAME, + type: TaskType.TERMINAL, + executionData: { + status: wfExecutionStatusToTaskStatus(executionStatus), + executed: + wfExecutionStatusToTaskStatus(executionStatus) !== TaskStatus.PENDING, + attempts: 0, + }, +}); + +export const terminalNode = (task: CommonTaskDef) => ({ + ...taskToNode(task, [], false), + ports: undefined, +}); + +export const firstTask = { + name: "start", + taskReferenceName: START_TASK_FAKE_TASK_REFERENCE_NAME, + type: TaskType.TERMINAL, +}; + +export const lastTask = { + name: "end", + taskReferenceName: END_TASK_FAKE_TASK_REFERENCE_NAME, + type: TaskType.TERMINAL, +}; + +export const startNode = taskToNode(firstTask); + +export const endNode = taskToNode(lastTask); + +export const processLastTask = ( + { + nodes = [], + edges = [], + previousTask, + previousTaskAllowsConnection = true, + }: NodesAndEdges & { + previousTask?: CommonTaskDef; + previousTaskAllowsConnection: boolean; + }, + executionStatus: WorkflowExecutionStatus = WorkflowExecutionStatus.RUNNING, +) => { + const pseudoEndTask = endPseudoTask(executionStatus); + const endNode: NodeData = terminalNode(pseudoEndTask); + const mappedEdges = edgeMapper( + pseudoEndTask, + previousTask, + previousTaskAllowsConnection, + ); + return { + nodes: nodes.concat(_isEmpty(mappedEdges) ? [] : endNode), // If there is no way to connect the endNode. then we dont put it + edges: edges.concat(mappedEdges), + }; +}; diff --git a/ui-next/src/components/flow/nodes/mapper/terminate.ts b/ui-next/src/components/flow/nodes/mapper/terminate.ts new file mode 100644 index 0000000000..3539f24b8b --- /dev/null +++ b/ui-next/src/components/flow/nodes/mapper/terminate.ts @@ -0,0 +1,26 @@ +import { extractExecutionDataOrEmpty } from "./common"; +import { BOTTOM_PORT_MARGIN, taskToSize } from "./layout"; +import { TerminateTaskDef, Crumb } from "types"; +import { NodeData } from "reaflow"; +import { NodeTaskData } from "./types"; + +export const taskToTerminateNode = ( + task: TerminateTaskDef, + crumbs: Crumb[] = [], +): NodeData> => { + const { taskReferenceName, name } = task; + const { width, height } = taskToSize(task); + return { + id: taskReferenceName, + text: name, + data: { + task, + crumbs, + ...extractExecutionDataOrEmpty(task), + }, + width, + // Add a bit of margin to the bottom + // to avoid overlapping arrow edges and ports + height: height + BOTTOM_PORT_MARGIN, + }; +}; diff --git a/ui-next/src/components/flow/nodes/mapper/types.ts b/ui-next/src/components/flow/nodes/mapper/types.ts new file mode 100644 index 0000000000..21203df8fd --- /dev/null +++ b/ui-next/src/components/flow/nodes/mapper/types.ts @@ -0,0 +1,38 @@ +import { NodeData, EdgeData } from "reaflow"; +import { + Crumb, + CommonTaskDef, + TaskStatus, + WorkflowDef, + ExecutionTask, +} from "types"; + +export interface NodeTaskData { + task: T; + previousTask?: CommonTaskDef; + crumbs: Crumb[]; + action?: string; + status?: TaskStatus; + originalTask?: T; + selected?: boolean; + attempts?: number; + withinExpandedSubWorkflow?: boolean; + outputData?: Record; + parentLoop?: ExecutionTask; +} + +type EdgeInnerData = { + unreachableEdge?: boolean; + status?: TaskStatus; +}; +export interface NodesAndEdges { + nodes: NodeData[]; + edges: EdgeData>[]; +} + +export type EdgeTaskData = EdgeData>; + +export type SubWorkflowFunction = ( + name: string, + version?: number, +) => Promise>; diff --git a/ui-next/src/components/flow/state/FlowActorContext.tsx b/ui-next/src/components/flow/state/FlowActorContext.tsx new file mode 100644 index 0000000000..ad099d9939 --- /dev/null +++ b/ui-next/src/components/flow/state/FlowActorContext.tsx @@ -0,0 +1,12 @@ +import { createContext, ReactNode } from "react"; +import { ActorRef } from "xstate"; +import { FlowEvents } from "./types"; + +export interface FlowContextProps { + flowActor?: ActorRef; + children?: ReactNode; +} + +export const FlowActorContext = createContext({ + flowActor: undefined, +}); diff --git a/ui-next/src/components/flow/state/action.ts b/ui-next/src/components/flow/state/action.ts new file mode 100644 index 0000000000..1191ceda9a --- /dev/null +++ b/ui-next/src/components/flow/state/action.ts @@ -0,0 +1,191 @@ +import { + assign, + DoneEvent, + DoneInvokeEvent, + forwardTo, + sendParent, +} from "xstate"; +import { applyNodeSelectionHelpr } from "./helpers"; +import _nth from "lodash/nth"; +import { FLOW_FINISHED_RENDERING } from "pages/definition/state/constants"; +import { + FlowActionTypes, + FlowContext, + ResetNodeSelectionEvent, + SelectEdgeEvent, + SelectNodeEvent, + OpenEdgeMenuEvent, + ToggleNodeMenuEvent, + UpdateWfDefinitionEvent, + StartDraggingNodeEvent, + StoppedDraggingNodeEvent, + UpdateCollapseWorkflowListEvent, + SelectTaskWithTaskRefEvent, +} from "./types"; +import { + END_TASK_FAKE_TASK_REFERENCE_NAME, + START_TASK_FAKE_TASK_REFERENCE_NAME, +} from "../nodes"; +import { ErrorInspectorEventTypes } from "pages/definition/errorInspector/state"; +import { DefinitionMachineEventTypes } from "pages/definition/state/types"; + +export const spreadData = assign>( + (__, { data }) => { + return data; + }, +); + +export const selectNode = assign( + ({ nodes }, { node: { id: selectNodeId, data } }) => { + if ( + selectNodeId === START_TASK_FAKE_TASK_REFERENCE_NAME || + selectNodeId === END_TASK_FAKE_TASK_REFERENCE_NAME || + data?.withinExpandedSubWorkflow === true + ) + return {}; + const newSelectedIndex = nodes.findIndex(({ id }) => id === selectNodeId); + + return { + selectedNodeIdx: newSelectedIndex, + nodes: applyNodeSelectionHelpr(nodes, newSelectedIndex), + }; + }, +); + +export const resetNodeSelection = assign({ + // Checking current editing task + // don't reset node in case error and let user stay at form to fix the error + selectedNodeIdx: (flowContext, __) => flowContext?.selectedNodeIdx, + nodes: ({ nodes }) => applyNodeSelectionHelpr(nodes, -1), // non existant index +}); + +export const updateCollapseWorkflowList = assign< + FlowContext, + UpdateCollapseWorkflowListEvent +>((ctx: any, event) => { + if (ctx && ctx.collapseWorkflowList) { + if (ctx.collapseWorkflowList.includes(event.workflowName)) { + const newCollapseWorkflowList = ctx.collapseWorkflowList.filter( + (item: string) => item !== event.workflowName, + ); + return { + collapseWorkflowList: newCollapseWorkflowList, + }; + } else { + const newCollapseWorkflowList = ctx.collapseWorkflowList.concat( + event.workflowName, + ); + return { + collapseWorkflowList: newCollapseWorkflowList, + }; + } + } + return { collapseWorkflowList: [event?.workflowName] }; +}); + +export const toggleEdgeMenu = assign({ + menuOperationContext: ({ menuOperationContext }, { edge }) => { + const r = menuOperationContext?.id === edge?.id ? undefined : edge; + return r; + }, +}); + +export const toggleNodeMenu = assign({ + openedNode: ({ openedNode }, { node }) => + openedNode?.id === node?.id ? undefined : node, +}); + +export const maybeCleanSelection = assign( + { + selectedNodeIdx: ({ selectedNodeIdx }, { cleanNodeSelection }) => { + return cleanNodeSelection ? undefined : selectedNodeIdx; + }, + }, +); + +export const notifyFinishedRender = sendParent((ctx: FlowContext) => { + return { + type: FLOW_FINISHED_RENDERING, + nodes: ctx.nodes, + node: _nth(ctx.nodes, ctx.selectedNodeIdx), + collapseWorkflowList: ctx.collapseWorkflowList, + }; +}); + +export const notifyErrorToParent = sendParent( + (ctx, { data }) => { + return { + type: ErrorInspectorEventTypes.REPORT_FLOW_ERROR, + ...data, + }; + }, +); + +export const notifySelectionToParent = sendParent< + FlowContext, + SelectNodeEvent | SelectEdgeEvent +>(({ nodes, selectedNodeIdx }, event) => { + if (event.type === FlowActionTypes.SELECT_NODE_EVT) { + return { + type: FlowActionTypes.SELECT_NODE_EVT, + node: _nth(nodes, selectedNodeIdx), + }; + } + + if (event.type === FlowActionTypes.SELECT_EDGE_EVT) { + return { + ...event, + }; + } + + return event; +}); + +export const notifyTaskSelectionToParent = sendParent< + FlowContext, + SelectTaskWithTaskRefEvent +>((_context, event) => { + return event; +}); + +export const forwardToZoom = forwardTo("panAndZoomMachine"); + +export const selectEdge = assign( + (_context, event) => { + return { + selectedEdge: event.edge, + }; + }, +); + +export const cleanMenuOperationContext = assign(() => ({ + menuOperationContext: undefined, +})); + +export const persistDraggedTask = assign({ + draggedNodeData: (__context, { nodeData }) => nodeData, +}); + +export const sendMoveTask = sendParent( + (__context, event) => { + return { + type: FlowActionTypes.MOVE_TASK_EVT, + sourceTask: event.fromData?.task, + sourceTaskCrumbs: event.fromData?.crumbs, + targetTask: event.toData?.task, + targetLocationCrumbs: event.toData?.crumbs, + position: event.toData?.position, + }; + }, +); + +export const clearDraggedElement = assign((__context) => ({ + draggedNodeData: undefined, +})); + +export const sendToDefinitionMachine = sendParent(() => { + return { + type: DefinitionMachineEventTypes.COLLAPSE_SIDEBAR_AND_RIGHT_PANEL, + onSelectNode: false, + }; +}); diff --git a/ui-next/src/components/flow/state/context.tsx b/ui-next/src/components/flow/state/context.tsx new file mode 100644 index 0000000000..45233f6caf --- /dev/null +++ b/ui-next/src/components/flow/state/context.tsx @@ -0,0 +1,14 @@ +import { FunctionComponent } from "react"; +import { FlowActorContext, FlowContextProps } from "./FlowActorContext"; + +export const FlowMachineContextProvider: FunctionComponent< + FlowContextProps +> = ({ flowActor, children }) => ( + + {children} + +); diff --git a/ui-next/src/components/flow/state/guards.ts b/ui-next/src/components/flow/state/guards.ts new file mode 100644 index 0000000000..7eb1d05138 --- /dev/null +++ b/ui-next/src/components/flow/state/guards.ts @@ -0,0 +1,15 @@ +import { FlowContext, StoppedDraggingNodeEvent } from "./types"; + +export const hasValidActiveAndCurrent = ( + __context: FlowContext, + event: StoppedDraggingNodeEvent, +) => { + const { fromData, toData } = event; + if (fromData === undefined || toData === undefined) { + return false; + } + if (fromData?.task?.taskReferenceName === toData?.task?.taskReferenceName) { + return false; + } + return true; +}; diff --git a/ui-next/src/components/flow/state/helpers.js b/ui-next/src/components/flow/state/helpers.js new file mode 100644 index 0000000000..7f8f1fe4b0 --- /dev/null +++ b/ui-next/src/components/flow/state/helpers.js @@ -0,0 +1,11 @@ +export const mergeInNodeData = (node, values) => ({ + ...node, + data: { ...node.data, ...values }, +}); + +export const applyNodeSelectionHelpr = (nodes, selectedNodeIdx) => + selectedNodeIdx == null + ? nodes + : nodes.map((node, idx) => + mergeInNodeData(node, { selected: idx === selectedNodeIdx }), + ); diff --git a/ui-next/src/components/flow/state/hook.ts b/ui-next/src/components/flow/state/hook.ts new file mode 100644 index 0000000000..777a3d81b3 --- /dev/null +++ b/ui-next/src/components/flow/state/hook.ts @@ -0,0 +1,135 @@ +import { useSelector } from "@xstate/react"; +import { ActorRef } from "xstate"; +import { useCallback } from "react"; +import { ElkRoot, EdgeData, NodeData } from "reaflow"; +import { + selectSelectedNode, + selectNodes, + selectEdges, + selectIsOpenedEdge, + selectOpenedNode, + selectWorkflowDefinition, + selectSelectedEdge, +} from "./selectors"; +import { FlowActionTypes, DraggedNodeData } from "./types"; +import { WorkflowDef } from "types/WorkflowDef"; + +export const useFlowMachine = (flowActor: ActorRef) => { + const send = flowActor.send; + + const selectNode = useCallback( + (node: NodeData) => + send({ + type: FlowActionTypes.SELECT_NODE_EVT, + node, + }), + [send], + ); + + const selectTaskWithTaskRef = useCallback( + (node: NodeData, exactTaskRef: string) => + send({ + type: FlowActionTypes.SELECT_TASK_WITH_TASK_REF, + node, + exactTaskRef, + }), + [send], + ); + + const selectEdge = ({ edge }: { edge: EdgeData }) => + send({ + type: FlowActionTypes.SELECT_EDGE_EVT, + edge, + }); + + const toggleEdgeMenu = useCallback( + (edge: EdgeData) => + send({ + type: FlowActionTypes.OPEN_EDGE_MENU_EVT, + edge, + }), + [send], + ); + + const toggleNodeMenu = useCallback( + (node: NodeData) => + send({ + type: FlowActionTypes.OPEN_NODE_MENU_EVT, + node, + }), + [send], + ); + + const updateWorkflowDefinition = useCallback( + (workflow: WorkflowDef) => + send({ + type: FlowActionTypes.UPDATE_WF_DEFINITION_EVT, + workflow, + }), + [send], + ); + + const handleSetLayout = (layout: ElkRoot) => { + send({ type: FlowActionTypes.SET_LAYOUT, layout }); + }; + + const draggingNodeStarts = (nodeData: DraggedNodeData) => { + send({ type: FlowActionTypes.DRAG_TASK_BEGIN, nodeData }); + }; + + const draggingNodeEnds = ( + fromData: DraggedNodeData, + toData: DraggedNodeData, + ) => { + send({ type: FlowActionTypes.DRAG_TASK_END, fromData, toData }); + }; + + const selectedNode = useSelector(flowActor, selectSelectedNode); + const selectedEdge = useSelector(flowActor, selectSelectedEdge); + const nodes = useSelector(flowActor, selectNodes); + const edges = useSelector(flowActor, selectEdges); + const openedEdge = useSelector(flowActor, selectIsOpenedEdge); + const openedNode = useSelector(flowActor, selectOpenedNode); + const workflowDefinition = useSelector(flowActor, selectWorkflowDefinition); + const panAndZoomActor = useSelector( + flowActor, + (state) => state.children?.panAndZoomMachine, + ); + const isInconsistent = useSelector(flowActor, (state) => + state.matches({ + init: { + diagramRenderer: "inconsistent", + }, + }), + ); + + const isShowDescription = useSelector(flowActor, (state) => + state.hasTag("showDescription"), + ); + + return [ + { + toggleEdgeMenu, + selectNode, + selectEdge, + toggleNodeMenu, + updateWorkflowDefinition, + draggingStarts: draggingNodeStarts, + draggingNodeEnds, + handleSetLayout, + selectTaskWithTaskRef, + }, + { + selectedNode, + selectedEdge, + nodes, + edges, + openedEdge, + openedNode, + isInconsistent, + workflowDefinition, + panAndZoomActor, + isShowDescription, + }, + ] as const; +}; diff --git a/ui-next/src/components/flow/state/index.ts b/ui-next/src/components/flow/state/index.ts new file mode 100644 index 0000000000..7108b490a8 --- /dev/null +++ b/ui-next/src/components/flow/state/index.ts @@ -0,0 +1,3 @@ +export * from "./hook"; +export * from "./types"; +export * from "./context"; diff --git a/ui-next/src/components/flow/state/machine.ts b/ui-next/src/components/flow/state/machine.ts new file mode 100644 index 0000000000..1fc6362eba --- /dev/null +++ b/ui-next/src/components/flow/state/machine.ts @@ -0,0 +1,242 @@ +import { createMachine } from "xstate"; +import * as actions from "./action"; +import * as guards from "./guards"; +import { updateWorkflowDefinitionService } from "./service"; +import { panAndZoomMachine } from "../components/graphs/PanAndZoomWrapper"; +import { + richAddTaskMenuMachine, + getALL_TASKS, + RichAddMenuTabs, + RichAddTaskMenuEventTypes, +} from "../components/RichAddTaskMenu"; + +import { + FlowContext, + FlowEvents, + FlowStates, + FlowActionTypes, + FlowMachineStates, +} from "./types"; + +export const flowMachine = createMachine( + { + id: "flowMachine", + predictableActionArguments: true, + initial: FlowMachineStates.INIT, + context: { + authHeaders: undefined, + currentWf: {}, + selectedNodeIdx: undefined, + nodes: [], + edges: [], + menuOperationContext: undefined, + openedNode: undefined, + draggedNodeData: undefined, + collapseWorkflowList: [], + }, + states: { + [FlowMachineStates.INIT]: { + type: "parallel", + states: { + [FlowMachineStates.DIAGRAM_RENDERER]: { + initial: "inconsistent", + on: { + [FlowActionTypes.SELECT_TASK_WITH_TASK_REF]: { + actions: ["notifyTaskSelectionToParent"], + }, + [FlowActionTypes.UPDATE_WF_DEFINITION_EVT]: { + target: ".updatingWfDefintion", + actions: ["maybeCleanSelection"], + }, + }, + states: { + inconsistent: { + entry: "resetNodeSelection", + }, + updatingWfDefintion: { + invoke: { + id: "updateWorkflowDefinition", + src: "updateWorkflowDefinitionService", + onDone: { + actions: ["spreadData"], + target: FlowMachineStates.DIAGRAM_RENDERER_INIT, + }, + + onError: { + target: "inconsistent", + actions: ["notifyErrorToParent"], + }, + }, + }, + [FlowMachineStates.DIAGRAM_RENDERER_INIT]: { + entry: ["cleanMenuOperationContext", "notifyFinishedRender"], + initial: FlowMachineStates.DIAGRAM_RENDERER_MENU_CLOSED, + on: { + [FlowActionTypes.SELECT_NODE_EVT]: { + actions: ["selectNode", "notifySelectionToParent"], + }, + [FlowActionTypes.SELECT_TASK_WITH_TASK_REF]: { + actions: ["notifyTaskSelectionToParent"], + }, + [FlowActionTypes.SELECT_NODE_INTERNAL_EVT]: { + actions: ["selectNode"], + }, + [FlowActionTypes.OPEN_NODE_MENU_EVT]: { + actions: "toggleNodeMenu", + }, + [FlowActionTypes.SET_READ_ONLY_EVT]: { + target: "inconsistent", + }, + [FlowActionTypes.SELECT_EDGE_EVT]: { + actions: ["selectEdge", "notifySelectionToParent"], + }, + [FlowActionTypes.UPDATE_COLLAPSE_WORKFLOW_LIST]: { + actions: ["updateCollapseWorkflowList"], + }, + [RichAddTaskMenuEventTypes.SET_MENU_TYPE]: { + actions: ["sendToDefinitionMachine"], + }, + }, + states: { + [FlowMachineStates.DIAGRAM_RENDERER_MENU_OPENED]: { + invoke: { + id: "richAddTaskMenuMachine", + src: richAddTaskMenuMachine, + data: ({ authHeaders, menuOperationContext, nodes }) => ({ + authHeaders, + operationContext: menuOperationContext, + taskDefinitions: [], + workflowDefinitions: [], + searchQuery: "", + nodes, + baseTaskMenuItems: getALL_TASKS(), + selectedTab: RichAddMenuTabs.ALL_TAB, + workerMenuItems: [], + workflowMenuItems: [], + menuType: "quick", + }), + onDone: { + actions: "cleanMenuOperationContext", + target: FlowMachineStates.DIAGRAM_RENDERER_MENU_CLOSED, + }, + }, + }, + [FlowMachineStates.DIAGRAM_RENDERER_MENU_CLOSED]: { + entry: ["clearDraggedElement"], + on: { + [FlowActionTypes.OPEN_EDGE_MENU_EVT]: { + actions: "toggleEdgeMenu", + target: FlowMachineStates.DIAGRAM_RENDERER_MENU_OPENED, + }, + [FlowActionTypes.DRAG_TASK_BEGIN]: { + actions: ["persistDraggedTask"], + target: + FlowMachineStates.DIAGRAM_RENDERER_BEGIN_DRAGGING, + }, + }, + }, + [FlowMachineStates.DIAGRAM_RENDERER_BEGIN_DRAGGING]: { + initial: "draggingTask", + states: { + draggingTask: { + on: { + [FlowActionTypes.DRAG_TASK_END]: [ + { + cond: "hasValidActiveAndCurrent", + target: `#flowMachine.${[ + FlowMachineStates.INIT, + FlowMachineStates.DIAGRAM_RENDERER, + FlowMachineStates.DIAGRAM_RENDERER_INIT, + ].join(".")}`, + actions: ["sendMoveTask"], + }, + { + target: `#flowMachine.${[ + FlowMachineStates.INIT, + FlowMachineStates.DIAGRAM_RENDERER, + FlowMachineStates.DIAGRAM_RENDERER_INIT, + ].join(".")}`, + }, + ], + }, + }, + }, + }, + }, + }, + }, + }, + zoomControls: { + initial: "opened", + states: { + opened: { + on: { + [FlowActionTypes.SET_LAYOUT]: { + actions: ["forwardToZoom"], + }, + [FlowActionTypes.SELECT_NODE_EVT]: { + actions: ["forwardToZoom"], + }, + [FlowActionTypes.DRAG_TASK_BEGIN]: { + actions: ["forwardToZoom"], + }, + [FlowActionTypes.DRAG_TASK_END]: { + actions: ["forwardToZoom"], + }, + [FlowActionTypes.RESET_ZOOM_POSITION]: { + actions: ["forwardToZoom"], + }, + }, + invoke: { + src: panAndZoomMachine, + id: "panAndZoomMachine", + }, + }, + }, + }, + cardDisplayType: { + initial: "init", + states: { + init: { + always: [ + { + target: "showDescription", + cond: (context) => { + const currentWf = context.currentWf as any; + return currentWf?.isTemplateDetail === true; + }, + }, + { + target: "hideDescription", + }, + ], + }, + showDescription: { + tags: ["showDescription"], + on: { + [FlowActionTypes.TOGGLE_SHOW_DESCRIPTION]: { + target: "hideDescription", + }, + }, + }, + hideDescription: { + on: { + [FlowActionTypes.TOGGLE_SHOW_DESCRIPTION]: { + target: "showDescription", + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + actions: actions as any, + services: { + updateWorkflowDefinitionService, + }, + guards: guards as any, + }, +); diff --git a/ui-next/src/components/flow/state/selectors.ts b/ui-next/src/components/flow/state/selectors.ts new file mode 100644 index 0000000000..a21295abd6 --- /dev/null +++ b/ui-next/src/components/flow/state/selectors.ts @@ -0,0 +1,24 @@ +import { State } from "xstate"; +import { FlowContext, FlowMachineStates } from "./types"; + +export const selectSelectedNode = (state: State) => + state.context.selectedNodeIdx !== undefined + ? state.context.nodes[state.context.selectedNodeIdx] + : undefined; +export const selectSelectedEdge = (state: State) => + state.context.selectedEdge; +export const selectNodes = (state: State) => state.context.nodes; +export const selectEdges = (state: State) => state.context.edges; +export const selectIsOpenedEdge = (state: State) => + state.matches([ + FlowMachineStates.INIT, + FlowMachineStates.DIAGRAM_RENDERER, + FlowMachineStates.DIAGRAM_RENDERER_INIT, + FlowMachineStates.DIAGRAM_RENDERER_MENU_OPENED, + ]); +export const selectOpenedNode = (state: State) => + state.context.openedNode; +export const selectWorkflowDefinition = (state: State) => + state.context.currentWf; +export const selectWorkflowName = (state: State) => + state.context.currentWf.name; diff --git a/ui-next/src/components/flow/state/service.ts b/ui-next/src/components/flow/state/service.ts new file mode 100644 index 0000000000..81bd724d9d --- /dev/null +++ b/ui-next/src/components/flow/state/service.ts @@ -0,0 +1,103 @@ +import { applyNodeSelectionHelpr } from "./helpers"; +import { processWorkflow } from "../nodes"; +import _property from "lodash/property"; +import _filter from "lodash/filter"; +import _includes from "lodash/includes"; +import { SEVERITY_ERROR } from "pages/definition/state/constants"; +import { FlowContext } from "./types"; +import { EdgeData, NodeData } from "reaflow"; +import { queryClient } from "../../../queryClient"; +import { fetchWithContext, fetchContextNonHook } from "plugins/fetch"; +import _isNil from "lodash/isNil"; +import { logger } from "utils"; +import { AuthHeaders } from "types/common"; + +const fetchContext = fetchContextNonHook(); + +const BASE_PATH = `/metadata/workflow/`; + +const fetchForWorkflowDefinition = async ({ + workflowName, + currentVersion, + authHeaders, + collapseWorkflowList, +}: { + workflowName: string; + currentVersion: string; + authHeaders: AuthHeaders; + collapseWorkflowList: string[]; +}) => { + const path = `${BASE_PATH}${workflowName}${ + _isNil(currentVersion) ? "" : `?version=${currentVersion}` + }`; + try { + if (collapseWorkflowList?.includes(workflowName)) { + const response = await queryClient.fetchQuery( + [fetchContext.stack, path], + () => fetchWithContext(path, fetchContext, { headers: authHeaders }), + ); + return response; + } + return { tasks: [] }; + } catch (error) { + logger.error("Error fetching for workflow definition ", error); + return Promise.reject({ + message: "Error searching for workflow definition", + }); + } +}; + +export const updateWorkflowDefinitionService = async ( + { selectedNodeIdx, authHeaders, collapseWorkflowList }: FlowContext, + { workflow, showPorts = true, workflowExecutionStatus = "" }: any, +): Promise< + | { + nodes: NodeData[]; + edges: EdgeData[]; + currentWf: any; + } + | { severity: "error"; text: string } +> => { + const expandSubWorkflow = showPorts; + + try { + const { nodes, edges } = await processWorkflow( + workflow, + showPorts, + expandSubWorkflow, // expand subworkflow + async (workflowName: string, version?: number) => + fetchForWorkflowDefinition({ + workflowName, + currentVersion: String(version), + authHeaders: authHeaders!, + collapseWorkflowList: collapseWorkflowList!, + }), + workflowExecutionStatus, + ); + const justTheIds = nodes.map(_property("id")); + const duplicates = _filter(justTheIds, (value, index, iteratee) => + _includes(iteratee, value, index + 1), + ); + + if (duplicates.length === 0) { + return { + nodes: applyNodeSelectionHelpr(nodes, selectedNodeIdx), + edges, + currentWf: workflow, + }; + } else { + return Promise.reject({ + severity: SEVERITY_ERROR, + text: `You can't repeat taskReferenceName you have the following duplicates ${duplicates.join( + ",", + )}`, + }); + } + } catch (error) { + console.error(error); + return Promise.reject({ + severity: SEVERITY_ERROR, + text: "Invalid Json can't process sync. Fix the JSON and try again", + }); + } +}; diff --git a/ui-next/src/components/flow/state/types.ts b/ui-next/src/components/flow/state/types.ts new file mode 100644 index 0000000000..9f01cae1ae --- /dev/null +++ b/ui-next/src/components/flow/state/types.ts @@ -0,0 +1,188 @@ +import { DoneInvokeEvent } from "xstate"; +import { NodeData, EdgeData, ElkRoot } from "reaflow"; +import { + AuthHeaders, + CommonTaskDef, + Crumb, + WorkflowExecutionStatus, + WorkflowDef, +} from "types"; +import { OperationContextData } from "../components/RichAddTaskMenu/state/types"; + +export enum FlowActionTypes { + SELECT_NODE_EVT = "SELECT_NODE_EVT", + SELECT_NODE_INTERNAL_EVT = "selectNodeInternal", + PERFORM_OPERATION_EVT = "performOperation", + OPEN_EDGE_MENU_EVT = "openEdgeMenu", + CLOSE_EDGE_MENU_EVT = "closeEdgeMenu", + OPEN_NODE_MENU_EVT = "openNodeMenu", + UPDATE_WF_DEFINITION_EVT = "updateWfDefinition", + SET_READ_ONLY_EVT = "SET_READ_ONLY_EVT", + PERFORM_OPERATION_DEBOUNCE = "performOperationDebounce", + CANCEL_DEBOUNCE_PERFORM_OPERATION = "cancelDebouncePerformOperation", + UPDATE_WF_METADATA = "updateWorkflowMetadata", + RESET_NODE_SELECTION = "resetNodeSelection", + SET_LAYOUT = "SET_LAYOUT", + SELECT_EDGE_EVT = "SELECT_EDGE_EVT", + RESET_ZOOM_POSITION = "RESET_ZOOM_POSITION", + CENTER_ON_SELECTED_TASK = "CENTER_ON_SELECTED_TASK", + FLOW_FINISHED_RENDERING = "FLOW_FINISHED_RENDERING", + SELECT_TASK_WITH_TASK_REF = "SELECT_TASK_WITH_TASK_REF", + + DRAG_TASK_BEGIN = "DRAG_TASK_BEGIN", + DRAG_TASK_END = "DRAG_TASK_END", + MOVE_TASK_EVT = "MOVE_TASK_EVT", + UPDATE_COLLAPSE_WORKFLOW_LIST = "UPDATE_COLLAPSE_WORKFLOW_LIST", + TOGGLE_SHOW_DESCRIPTION = "TOGGLE_SHOW_DESCRIPTION", +} + +export type DraggedNodeData = { + task: CommonTaskDef; + crumbs: Crumb[]; + height?: number; + width?: number; +}; + +export type DropPosition = { position: "ABOVE" | "BELOW" }; + +export interface FlowContext { + currentWf: Partial; + selectedNodeIdx?: number; + nodes: NodeData[]; + edges: EdgeData[]; + menuOperationContext?: OperationContextData; + openedNode?: NodeData; + layout?: ElkRoot; + authHeaders?: AuthHeaders; + selectedEdge?: EdgeData; + draggedNodeData?: DraggedNodeData; + collapseWorkflowList?: string[]; +} +export type SelectNodeEvent = { + type: FlowActionTypes.SELECT_NODE_EVT; + node: NodeData; +}; + +export type SelectTaskWithTaskRefEvent = { + type: FlowActionTypes.SELECT_TASK_WITH_TASK_REF; + node: NodeData; + exactTaskRef: string; +}; + +export type SelectEdgeEvent = { + type: FlowActionTypes.SELECT_EDGE_EVT; + node: NodeData; + edge: EdgeData; +}; + +export type ResetNodeSelectionEvent = { + type: FlowActionTypes.RESET_NODE_SELECTION; +}; + +export type PerformOperationEvent = { + type: FlowActionTypes.PERFORM_OPERATION_EVT; + operation: any; + task: any; + crumbs: string[]; +}; + +export type OpenEdgeMenuEvent = { + type: FlowActionTypes.OPEN_EDGE_MENU_EVT; + edge?: OperationContextData; +}; + +export type CloseEdgeMenuEvent = { + type: FlowActionTypes.CLOSE_EDGE_MENU_EVT; +}; + +export type ToggleNodeMenuEvent = { + type: FlowActionTypes.OPEN_NODE_MENU_EVT; + node?: NodeData; +}; + +export type UpdateWfDefinitionEvent = { + type: FlowActionTypes.UPDATE_WF_DEFINITION_EVT; + workflow: any; + cleanNodeSelection: boolean; + workflowExecutionStatus?: WorkflowExecutionStatus; + showPorts?: boolean; +}; + +export type UpdateCollapseWorkflowListEvent = { + type: FlowActionTypes.UPDATE_COLLAPSE_WORKFLOW_LIST; + workflowName: string; +}; + +export type SetLayoutEvent = { + type: FlowActionTypes.SET_LAYOUT; + layout: ElkRoot; +}; + +export type ResetZoomPositionEvent = { + type: FlowActionTypes.RESET_ZOOM_POSITION; +}; + +export type SetCenterPositionEvent = { + type: FlowActionTypes.CENTER_ON_SELECTED_TASK; +}; + +export type StartDraggingNodeEvent = { + type: FlowActionTypes.DRAG_TASK_BEGIN; + nodeData: DraggedNodeData; +}; + +export type StoppedDraggingNodeEvent = { + type: FlowActionTypes.DRAG_TASK_END; + fromData?: DraggedNodeData; + toData?: DraggedNodeData & DropPosition; +}; + +export type ToggleShowDescriptionEvent = { + type: FlowActionTypes.TOGGLE_SHOW_DESCRIPTION; +}; + +export type FlowEvents = + | SelectNodeEvent + | SelectEdgeEvent + | SelectTaskWithTaskRefEvent + | PerformOperationEvent + | OpenEdgeMenuEvent + | CloseEdgeMenuEvent + | SetLayoutEvent + | ToggleNodeMenuEvent + | UpdateWfDefinitionEvent + | ResetNodeSelectionEvent + | ResetZoomPositionEvent + | StoppedDraggingNodeEvent + | StartDraggingNodeEvent + | SetCenterPositionEvent + | UpdateCollapseWorkflowListEvent + | DoneInvokeEvent + | ToggleShowDescriptionEvent; + +export enum FlowMachineStates { + INIT = "init", + DIAGRAM_RENDERER = "diagramRenderer", + DIAGRAM_RENDERER_INIT = "diagramRenderer_init", + DIAGRAM_RENDERER_BEGIN_DRAGGING = "diagramRenderer_beginDragging", + DIAGRAM_RENDERER_MENU_CLOSED = "diagramRenderer_menuClosed", + DIAGRAM_RENDERER_MENU_OPENED = "diagramRenderer_menuOpened", +} + +export type FlowStates = + | { + value: "idle"; + context: FlowContext; + } + | { + value: "updatingWfDefinition"; + context: FlowContext; + } + | { + value: "init"; + context: FlowContext; + } + | { + value: "notifyParent"; + context: FlowContext; + }; diff --git a/ui-next/src/components/flow/testUtils.js b/ui-next/src/components/flow/testUtils.js new file mode 100644 index 0000000000..dc60f2ec15 --- /dev/null +++ b/ui-next/src/components/flow/testUtils.js @@ -0,0 +1,45 @@ +export const stressGraph = (workflowDefinition, repetitions) => { + const newWorkflow = { ...workflowDefinition }; + newWorkflow.tasks = repeatTasks(newWorkflow.tasks, repetitions); + return newWorkflow; +}; + +const repeatTasks = (tasks, repetitions) => + Array.from({ length: repetitions }, (v, i) => { + return tasks.map((task) => { + const newTask = { ...task }; + newTask.name = `${task.name}_${i}`; + newTask.taskReferenceName = `${task.taskReferenceName}_${i}`; + + if (task.forkTasks) { + newTask.forkTasks = task.forkTasks.map((forkTask) => { + const newForkTask = forkTask.map((forkTaskItem) => { + const newForkTaskItem = { ...forkTaskItem }; + newForkTaskItem.name = `${forkTaskItem.name}_${i}`; + newForkTaskItem.taskReferenceName = `${forkTaskItem.taskReferenceName}_${i}`; + + if (forkTaskItem.loopOver) { + newForkTaskItem.loopOver = forkTaskItem.loopOver.map( + (loopOverItem) => { + const newLoopOverItem = { ...loopOverItem }; + newLoopOverItem.name = `${loopOverItem.name}_${i}`; + newLoopOverItem.taskReferenceName = `${loopOverItem.taskReferenceName}_${i}`; + return newLoopOverItem; + }, + ); + } + + return newForkTaskItem; + }); + return newForkTask; + }); + + if (task.joinOn?.length > 0) { + newTask.joinOn = task.joinOn.map((name) => { + return `${name}_${i}`; + }); + } + } + return newTask; + }); + }).flat(); diff --git a/ui-next/src/components/flow/theme.ts b/ui-next/src/components/flow/theme.ts new file mode 100644 index 0000000000..729090810c --- /dev/null +++ b/ui-next/src/components/flow/theme.ts @@ -0,0 +1,129 @@ +import { TaskType } from "types"; +import { TaskStatus } from "../../types/TaskStatus"; +import { colors } from "theme/tokens/variables"; + +const DEFAULT_NODE_WIDTH = 350; +const DEFAULT_NODE_HEIGHT = 100; +const DO_WHILE_PADDING = 30; + +export const getFlowTheme = (mode = "light") => ({ + nodeTypes: { + DEFAULT: { + width: DEFAULT_NODE_WIDTH, + height: DEFAULT_NODE_HEIGHT, + }, + [TaskType.DO_WHILE]: { + padding: DO_WHILE_PADDING, + width: DEFAULT_NODE_WIDTH + DO_WHILE_PADDING * 2, + height: 450, + itemHeight: 150, + }, + [TaskType.SWITCH]: { width: DEFAULT_NODE_WIDTH + 100, height: 200 }, + [TaskType.DECISION]: { width: DEFAULT_NODE_WIDTH, height: 200 }, + [TaskType.KAFKA_PUBLISH]: { width: DEFAULT_NODE_WIDTH, height: 150 }, + [TaskType.JSON_JQ_TRANSFORM]: { + width: DEFAULT_NODE_WIDTH, + height: 140, + }, + [TaskType.HTTP]: { width: DEFAULT_NODE_WIDTH, height: 130 }, + [TaskType.EVENT]: { width: DEFAULT_NODE_WIDTH, height: 130 }, + [TaskType.WAIT]: { + width: DEFAULT_NODE_WIDTH, + height: 110, + }, + [TaskType.FORK_JOIN]: { width: DEFAULT_NODE_WIDTH, height: 80 }, + [TaskType.FORK_JOIN_DYNAMIC]: { + width: DEFAULT_NODE_WIDTH, + height: 120, + }, + [TaskType.TERMINAL]: { width: 80, height: 80 }, + [TaskType.SWITCH_JOIN]: { width: 350, height: 55 }, + FORK_JOIN_COLLAPSED: { width: DEFAULT_NODE_WIDTH, height: 140 }, + [TaskType.TASK_SUMMARY]: { + width: DEFAULT_NODE_WIDTH + DO_WHILE_PADDING * 2, + height: 50, + }, + }, + taskStatusOutline: { + [TaskStatus.COMPLETED]: colors.primaryGreen, + [TaskStatus.COMPLETED_WITH_ERRORS]: "#EEAA00", + [TaskStatus.CANCELED]: "#fba404", + [TaskStatus.FAILED]: "#DD2222", + [TaskStatus.FAILED_WITH_TERMINAL_ERROR]: "#DD2222", + [TaskStatus.TIMED_OUT]: "#DD2222", + [TaskStatus.IN_PROGRESS]: "#999999", + [TaskStatus.SCHEDULED]: "#999999", + [TaskStatus.SKIPPED]: "#F5BF42", + [TaskStatus.PENDING]: "transparent", + [TaskStatus.NULL]: "transparent", + }, + graph: { + handleBorderColor: "#585a68", + handleSize: 8, + backgroundColor: "#e6e6e6", + }, + taskCard: { + selected: { + outlineColor: "#3388DD", + boxShadow: "none", + }, + operators: { + background: "#205668", + text: "white", + }, + systemTasks: { + background: "white", + color: "#111111", + }, + cardLabel: { + background: "#dddddd", + color: "black", + }, + addPathButton: { + background: "#eeeeee", + text: "black", + hoverBackground: "#dddddd", + }, + deleteButton: { + iconColor: "#DD2222", + background: "#f0f0f0", + }, + switchAdd: { + iconColor: "black", + background: "#f9f53d", + }, + }, + terminalTask: { + ...(mode === "dark" + ? { + color: colors.gray14, + background: "#3a3929", + border: "5px solid rgb(67 107 120)", + } + : { + color: colors.gray00, + background: "#ffffff", + border: "5px solid rgb(114 164 180)", + }), + }, + decisionOperator: { + caseLabel: { + defaultCaseBackground: "rgb(225 243 255)", + background: "rgb(225 243 255)", + }, + }, + edges: { + default: { + stroke: "#757575", + strokeWidth: 1, + }, + completed: { + stroke: colors.primaryGreen, + strokeWidth: 2, + }, + }, +}); + +const theme = getFlowTheme(); + +export default theme; diff --git a/ui-next/src/components/index.ts b/ui-next/src/components/index.ts new file mode 100644 index 0000000000..0d769e0a72 --- /dev/null +++ b/ui-next/src/components/index.ts @@ -0,0 +1,36 @@ +// Buttons +export { default as AutoRefreshButton } from "./AutoRefreshButton"; +export { default as ButtonGroup } from "./ButtonGroup"; +export { default as DropdownButton } from "./DropdownButton"; +export { default as Button } from "./MuiButton"; +export { default as IconButton } from "./MuiIconButton"; +export { default as SplitButton } from "./SplitButton"; + +// Layout +export { default as Paper } from "./Paper"; +export { Tab, default as Tabs } from "./Tabs"; + +// Text +export { default as Dropdown } from "./Dropdown"; +export { default as Heading } from "./Heading"; +export { default as Input } from "./Input"; +export { default as Typography } from "./MuiTypography"; +export { default as NavLink } from "./NavLink"; +export { default as Select } from "./Select"; +export { default as Text } from "./Text"; + +// Tables +export { default as DataTable } from "./DataTable/DataTable"; +export { default as KeyValueTable } from "./KeyValueTable"; +export { default as ReactJson } from "./ReactJson"; + +// Misc +export { default as ProgressHeading } from "./Header"; +export { default as LinearProgress } from "./LinearProgress"; + +export * from "./InputNumber"; + +export * from "./SubjectSelector"; + +export * from "./AutoCompleteWithDescription"; +export * from "./CodeBlockInput"; diff --git a/ui-next/src/components/reactHookForm/ReactHookFormDropdown.tsx b/ui-next/src/components/reactHookForm/ReactHookFormDropdown.tsx new file mode 100644 index 0000000000..369a7dbc2a --- /dev/null +++ b/ui-next/src/components/reactHookForm/ReactHookFormDropdown.tsx @@ -0,0 +1,50 @@ +import { Dropdown } from "components"; +import { Control, Controller, FieldPath, FieldValues } from "react-hook-form"; + +interface IReactHookFormDropdown< + TFieldValues extends FieldValues, + TName extends FieldPath, +> { + control: Control; + defaultValue?: any; + freeSolo?: boolean; + fullWidth?: boolean; + label: string; + multiple?: boolean; + name: TName; + required: boolean; + options: string[] | number[] | Array<{ label: string }>; + error?: boolean; + helperText?: any; + id?: string; + getOptionLabel?: (option?: any) => any; +} + +export default function ReactHookFormDropdown< + T extends FieldValues, + TN extends FieldPath, +>({ + control, + defaultValue, + name, + required, + ...props +}: IReactHookFormDropdown) { + return ( + ( + field.onChange(item)} + required={required} + /> + )} + rules={{ required: required }} + /> + ); +} diff --git a/ui-next/src/components/reactHookForm/ReactHookFormInput.tsx b/ui-next/src/components/reactHookForm/ReactHookFormInput.tsx new file mode 100644 index 0000000000..5b396e418b --- /dev/null +++ b/ui-next/src/components/reactHookForm/ReactHookFormInput.tsx @@ -0,0 +1,63 @@ +import { Input } from "components"; +import { Control, Controller, FieldPath, FieldValues } from "react-hook-form"; + +interface IReactHookFormInput< + TFieldValues extends FieldValues, + TName extends FieldPath, +> { + control: Control; + defaultValue?: any; + error?: boolean; + fullWidth?: boolean; + helperText?: string | any; + label?: string; + placeholder?: string; + name: TName; + required: boolean; + readOnlyInput?: boolean; + id?: string; + spellCheck?: boolean; + multiline?: boolean; + minRows?: number | string; + maxRows?: number | string; +} + +export default function ReactHookFormInput< + T extends FieldValues, + TN extends FieldPath, +>({ + control, + defaultValue, + error, + fullWidth, + helperText, + label, + placeholder, + name, + required, + readOnlyInput, + ...props +}: IReactHookFormInput) { + return ( + ( + field.onChange(value)} + /> + )} + rules={{ required: required }} + /> + ); +} diff --git a/ui-next/src/components/searchWrapper/SearchWrapper.tsx b/ui-next/src/components/searchWrapper/SearchWrapper.tsx new file mode 100644 index 0000000000..6876ddf79e --- /dev/null +++ b/ui-next/src/components/searchWrapper/SearchWrapper.tsx @@ -0,0 +1,81 @@ +import SearchIcon from "@mui/icons-material/Search"; +import { Stack } from "@mui/material"; +import MuiTypography from "components/MuiTypography"; +import SearchEverything from "components/SearchEverything"; +import UIModal from "components/UIModal"; +import HotKeysButton from "components/Sidebar/HotKeysButton"; +import { Fragment } from "react"; +import { colors } from "theme/tokens/variables"; +import { useSearchMachine } from "./state/hook"; + +export interface SearchModalProps { + open: boolean; + setOpen: (val: boolean) => void; +} +const props = { + title: "Search", + icon: , + description: "Search the platform for all your definitions in one place", + enableCloseButton: true, +}; + +function SearchEverythingModal({ open, setOpen }: SearchModalProps) { + const [{ searchTerm, searchResults }, { setSearchTerm }] = useSearchMachine(); + + return ( + + + ↵]} + /> + + to select + + + + ↑, + , + ]} + /> + + to navigate + + + + + + to close + + + + } + footerSx={{ + p: 1.5, + justifyContent: "center", + color: colors.greyText2, + backgroundColor: colors.sidebarBarelyPastWhite, + borderTop: "none", + }} + > + setSearchTerm!("")} + {...(searchResults && { searchResults: searchResults })} + setOpen={setOpen} + maxSearchResults={5} + /> + + ); +} + +export default SearchEverythingModal; diff --git a/ui-next/src/components/searchWrapper/state/actions.ts b/ui-next/src/components/searchWrapper/state/actions.ts new file mode 100644 index 0000000000..a575cfefd7 --- /dev/null +++ b/ui-next/src/components/searchWrapper/state/actions.ts @@ -0,0 +1,46 @@ +import { assign, DoneInvokeEvent } from "xstate"; +import { PersistSearchTermEvent, SearchMachineContext } from "./types"; +import { WorkflowDef } from "types/WorkflowDef"; + +export const persistSearchTerm = assign< + SearchMachineContext, + PersistSearchTermEvent +>({ + searchTerm: (_, { searchTerm }) => searchTerm, + maxSearchResults: (_, { count }) => count, +}); + +export const persistTaskNames = assign< + SearchMachineContext, + DoneInvokeEvent<{ name: string; description?: string }[]> +>({ + taskDefinitions: (_, { data }) => data, +}); + +export const persistWorkflowNames = assign< + SearchMachineContext, + DoneInvokeEvent +>({ + workflowDefinitions: (_, { data }) => data, +}); + +export const persistScheduleNames = assign< + SearchMachineContext, + DoneInvokeEvent +>({ + schedulers: (_, { data }) => data, +}); + +export const persistEventNames = assign< + SearchMachineContext, + DoneInvokeEvent +>({ + events: (_, { data }) => data, +}); + +export const persistErrorMessage = assign< + SearchMachineContext, + DoneInvokeEvent<{ message: string }> +>({ + error: (_context, { data }) => ({ ...data, severity: "error" }), +}); diff --git a/ui-next/src/components/searchWrapper/state/helpers.test.ts b/ui-next/src/components/searchWrapper/state/helpers.test.ts new file mode 100644 index 0000000000..64b5e81924 --- /dev/null +++ b/ui-next/src/components/searchWrapper/state/helpers.test.ts @@ -0,0 +1,407 @@ +import { MenuItemType } from "components/Sidebar/types"; +import { flattenMenu, searchResultExtractor } from "./helpers"; + +const taskDefinitions = [ + { name: "something", description: "somthing ready" }, + { name: "eac_sca", description: "cool value" }, + { name: "najeeb_test", description: "breeze is cold" }, +]; + +const workflowDefinitions = [ + { + updateTime: 1692226077142, + name: "amqp_1", + description: + "Edit or extend this sample workflow. Set the workflow name to get started", + version: 1, + failureWorkflow: "", + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: false, + ownerEmail: "vasiliy.pankov@orkes.io", + timeoutSeconds: 0, + tasks: [], + }, + { + updateTime: 1692226077142, + name: "workflow_cool", + description: + "Edit or extend this sample workflow. Set the workflow name to get started", + version: 1, + failureWorkflow: "", + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: false, + ownerEmail: "najeeb.thangal@orkes.io", + timeoutSeconds: 0, + tasks: [], + }, + { + updateTime: 1692226077142, + name: "workflow_cool", + description: + "Edit or extend this sample workflow. Set the workflow name to get started", + version: 2, + failureWorkflow: "", + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: false, + ownerEmail: "najeeb.thangal@orkes.io", + timeoutSeconds: 0, + tasks: [], + }, + { + updateTime: 1692226077142, + name: "workflow_cool", + description: + "Edit or extend this sample workflow. Set the workflow name to get started", + version: 3, + failureWorkflow: "", + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: false, + ownerEmail: "najeeb.thangal@orkes.io", + timeoutSeconds: 0, + tasks: [], + }, + { + updateTime: 1692226077142, + name: "new workflow", + description: + "Edit or extend this sample workflow. Set the workflow name to get started", + version: 1, + failureWorkflow: "", + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: false, + ownerEmail: "najeeb.thangal@orkes.io", + timeoutSeconds: 0, + tasks: [], + }, +]; +const scheduler = ["sheducle", "new", "raju_schedule"]; + +describe("Check SearchResultExtractor function", () => { + it("Should return the expected result for core OSS categories", () => { + const searchTerm = "test"; + const expectedResult = [ + { + title: "Task Definitions", + route: "/taskDef", + sub: [ + { + route: "/taskDef", + title: "View all task definitions", + }, + { + route: "/taskDef/najeeb_test", + title: "najeeb_test", + }, + ], + }, + { + title: "Workflows", + route: "/workflowDef", + sub: [ + { + route: "/workflowDef", + title: "View all workflow definitions", + }, + ], + }, + { + title: "Schedules", + route: "/scheduleDef", + sub: [ + { + route: "/scheduleDef", + title: "View all schedulers", + }, + ], + }, + { + title: "Events", + route: "/eventHandlerDef", + sub: [ + { + route: "/eventHandlerDef", + title: "View all events", + }, + ], + }, + ]; + + const validation = searchResultExtractor({ + taskDefinitions, + searchTerm, + }); + expect(expectedResult).toEqual(validation); + }); + + it("Should return the results with workflow_cool", () => { + const searchTerm = "workflow_cool"; + const expected = [ + { + title: "Workflows", + route: "/workflowDef", + sub: [ + { + route: "/workflowDef", + title: "View all workflow definitions", + }, + { + route: "/workflowDef/workflow_cool", + title: "workflow_cool", + }, + ], + }, + { + title: "Task Definitions", + route: "/taskDef", + sub: [ + { + route: "/taskDef", + title: "View all task definitions", + }, + ], + }, + { + title: "Schedules", + route: "/scheduleDef", + sub: [ + { + route: "/scheduleDef", + title: "View all schedulers", + }, + ], + }, + { + title: "Events", + route: "/eventHandlerDef", + sub: [ + { + route: "/eventHandlerDef", + title: "View all events", + }, + ], + }, + ]; + + const validation = searchResultExtractor({ + taskDefinitions, + workflowDefinitions, + scheduler, + searchTerm, + }); + + expect(expected).toEqual(validation); + }); + + it("Should return empty array when no matches", () => { + const searchTerm = "hduauduhaehfahhaehfaehihiufhaihahfhaehfahehaiu"; + const validation = searchResultExtractor({ + taskDefinitions, + searchTerm, + }); + + const viewAllAsResults = [ + { + title: "Workflows", + route: "/workflowDef", + sub: [ + { + route: "/workflowDef", + title: "View all workflow definitions", + }, + ], + }, + { + title: "Task Definitions", + route: "/taskDef", + sub: [ + { + route: "/taskDef", + title: "View all task definitions", + }, + ], + }, + { + title: "Schedules", + route: "/scheduleDef", + sub: [ + { + route: "/scheduleDef", + title: "View all schedulers", + }, + ], + }, + { + title: "Events", + route: "/eventHandlerDef", + sub: [ + { + route: "/eventHandlerDef", + title: "View all events", + }, + ], + }, + ]; + + expect(viewAllAsResults).toEqual(validation); + }); + + it("Should return null", () => { + const searchTerm = ""; + const validation = searchResultExtractor({ + taskDefinitions, + searchTerm, + }); + + expect(null).toEqual(validation); + }); +}); + +const menuCases: { + description: string; + menuItems: MenuItemType[]; + expected: { route: string; title: string }[]; +}[] = [ + { + description: "Menu doesn't have nested & hidden menu items", + menuItems: [ + { + id: "menuA", + title: "Test menu A", + linkTo: "/test-menu-a", + shortcuts: [], + icon: "", + hidden: false, + }, + { + id: "menuB", + title: "Test menu B", + linkTo: "/test-menu-b", + shortcuts: [], + icon: "", + hidden: false, + }, + ], + expected: [ + { + title: "Test menu A", + route: "/test-menu-a", + }, + { + title: "Test menu B", + route: "/test-menu-b", + }, + ], + }, + { + description: "Menu without nested menu items, has hidden items", + menuItems: [ + { + id: "menuA", + title: "Test menu A", + linkTo: "/test-menu-a", + shortcuts: [], + icon: "", + hidden: false, + }, + { + id: "menuB", + title: "Test menu B", + linkTo: "/test-menu-b", + shortcuts: [], + icon: "", + hidden: true, + }, + { + id: "menuC", + title: "Test menu C", + linkTo: "/test-menu-c", + shortcuts: [], + icon: "", + hidden: true, + }, + ], + expected: [ + { + title: "Test menu A", + route: "/test-menu-a", + }, + ], + }, + { + description: "Menu has nested and hidden items", + menuItems: [ + { + id: "menuA", + title: "Test menu A", + linkTo: "/test-menu-a", + shortcuts: [], + icon: "", + hidden: false, + items: [ + { + id: "menuA1", + title: "Test menu A1", + linkTo: "/test-menu-a1", + shortcuts: [], + icon: "", + hidden: false, + }, + { + id: "menuA2", + title: "Test menu A2", + linkTo: "/test-menu-a2", + shortcuts: [], + icon: "", + hidden: true, + }, + { + id: "menuA3", + title: "Test menu A3", + linkTo: "/test-menu-a3", + shortcuts: [], + icon: "", + hidden: false, + }, + ], + }, + { + id: "menuB", + title: "Test menu B", + linkTo: "/test-menu-b", + shortcuts: [], + icon: "", + hidden: false, + }, + ], + expected: [ + { + title: "Test menu A - Test menu A1", + route: "/test-menu-a1", + }, + { + title: "Test menu A - Test menu A3", + route: "/test-menu-a3", + }, + { + title: "Test menu B", + route: "/test-menu-b", + }, + ], + }, +]; + +describe("Check flattenMenu function", () => { + test.each(menuCases)( + "Testing case: $description", + ({ menuItems, expected }) => { + const result = flattenMenu(menuItems); + + expect(result).toMatchObject(expected); + }, + ); +}); diff --git a/ui-next/src/components/searchWrapper/state/helpers.ts b/ui-next/src/components/searchWrapper/state/helpers.ts new file mode 100644 index 0000000000..a661c33b6e --- /dev/null +++ b/ui-next/src/components/searchWrapper/state/helpers.ts @@ -0,0 +1,228 @@ +/** + * Search helpers for the core OSS search machine. + * + * This file handles fuzzy search and result formatting for the core OSS + * searchable categories: workflows, task definitions, schedulers, and events. + * + * Enterprise categories (users, groups, applications, webhooks, integrations, + * prompts, user forms) are handled by enterprise plugins via searchProviders. + */ + +import { MenuItemType } from "components/Sidebar/types"; +import fastDeepEqual from "fast-deep-equal"; +import Fuse from "fuse.js"; +import { CommonDef } from "./types"; +import { getUniqueWorkflows } from "utils/workflow"; +import { WorkflowDef } from "types/WorkflowDef"; +import _isEmpty from "lodash/isEmpty"; +import _identity from "lodash/identity"; +import _prop from "lodash/fp/prop"; +import { + EVENT_HANDLERS_URL, + SCHEDULER_DEFINITION_URL, + TASK_DEF_URL, + WORKFLOW_DEFINITION_URL, +} from "utils/constants/route"; + +export interface SearchResultExtractorProps { + taskDefinitions?: CommonDef[]; + workflowDefinitions?: WorkflowDef[]; + scheduler?: string[]; + events?: string[]; + searchTerm: string; + maxSearchResults?: number; +} + +export const searchFunction = ( + targets: CommonDef[] | string[], + searchTerm: string, + maxSearchResults?: number, + keys?: string[], +) => { + const fuseInstance = new Fuse(targets, { + includeScore: false, + threshold: 0.2, // https://www.fusejs.io/api/options.html#threshold + ...(keys && { keys: keys }), + }); + const searchResults = fuseInstance.search(searchTerm ?? ""); + const limitedSearchResults = () => { + if (maxSearchResults) { + return searchResults && searchResults.length > maxSearchResults + ? searchResults.slice(0, maxSearchResults) + : searchResults; + } + return searchResults; + }; + return limitedSearchResults().map(({ item }) => item); +}; + +const fromName = _prop("name"); + +const allWhenSearchTerm = + (searchTerm: string) => + ( + items: Array = [], + config: { + routePrefix: string; + viewAllTitle: string; + toSuffix?: (a: string | CommonDef) => string; + toLabel?: (a: string | CommonDef) => string; + }, + ) => { + const { + routePrefix, + viewAllTitle, + toSuffix = _identity, + toLabel = _identity, + } = config; + if (!_isEmpty(searchTerm)) { + return [ + { route: routePrefix, title: viewAllTitle }, + ...items.map((item) => { + return { + route: `${routePrefix}/${toSuffix(item)}`, + title: toLabel(item) as string, + }; + }), + ]; + } + + return []; + }; + +export const searchResultExtractor = ({ + taskDefinitions, + workflowDefinitions, + scheduler, + events, + searchTerm, + maxSearchResults, +}: SearchResultExtractorProps) => { + let taskSearchResult; + let wfSearchResult; + let schedulerSearchResult; + let eventsSearchResult; + + if (taskDefinitions && taskDefinitions.length > 0) { + taskSearchResult = searchFunction( + taskDefinitions, + searchTerm, + maxSearchResults, + ["name", "description"], + ); + } + + if (workflowDefinitions && workflowDefinitions.length > 0) { + wfSearchResult = searchFunction( + getUniqueWorkflows(workflowDefinitions), + searchTerm, + maxSearchResults, + ["name", "description"], + ); + } + + if (scheduler && scheduler.length > 0) { + schedulerSearchResult = searchFunction( + scheduler, + searchTerm, + maxSearchResults, + ); + } + + if (events && events.length > 0) { + eventsSearchResult = searchFunction(events, searchTerm, maxSearchResults); + } + + const searchResultsToRoutes = allWhenSearchTerm(searchTerm); + + const taskDefinitionsSub = searchResultsToRoutes(taskSearchResult, { + routePrefix: TASK_DEF_URL.BASE, + viewAllTitle: "View all task definitions", + toSuffix: fromName, + toLabel: fromName, + }); + + const workflowDefinitionsSub = searchResultsToRoutes(wfSearchResult, { + routePrefix: WORKFLOW_DEFINITION_URL.BASE, + viewAllTitle: "View all workflow definitions", + toSuffix: fromName, + toLabel: fromName, + }); + + const schedulerSub = searchResultsToRoutes(schedulerSearchResult, { + routePrefix: SCHEDULER_DEFINITION_URL.BASE, + viewAllTitle: "View all schedulers", + }); + + const eventsSub = searchResultsToRoutes(eventsSearchResult, { + routePrefix: EVENT_HANDLERS_URL.BASE, + viewAllTitle: "View all events", + }); + + const emptyOutput = [ + { title: "Workflows", sub: [], route: WORKFLOW_DEFINITION_URL.BASE }, + { title: "Task Definitions", sub: [], route: TASK_DEF_URL.BASE }, + { title: "Schedules", sub: [], route: SCHEDULER_DEFINITION_URL.BASE }, + { title: "Events", sub: [], route: EVENT_HANDLERS_URL.BASE }, + ]; + + const dataOutput = [ + { + title: "Workflows", + route: WORKFLOW_DEFINITION_URL.BASE, + sub: workflowDefinitionsSub ?? [], + }, + { + title: "Task Definitions", + route: TASK_DEF_URL.BASE, + sub: taskDefinitionsSub ?? [], + }, + { + title: "Schedules", + route: SCHEDULER_DEFINITION_URL.BASE, + sub: schedulerSub ?? [], + }, + { + title: "Events", + route: EVENT_HANDLERS_URL.BASE, + sub: eventsSub ?? [], + }, + ].sort(({ sub: subA }, { sub: subB }) => subB.length - subA.length); + + if (searchTerm === "") { + return null; + } + + if (fastDeepEqual(emptyOutput, dataOutput)) { + return []; + } + + return dataOutput; +}; + +export const flattenMenu = ( + menuItems: MenuItemType[], + parentTitle?: string, +) => { + const result: { route: string; title: string }[] = []; + + menuItems.forEach(({ title, items, linkTo, hidden }) => { + if (!hidden) { + if (items && items.length > 0) { + result.push(...flattenMenu(items, title)); + + return; + } + + const tempTitle = parentTitle ? `${parentTitle} - ${title}` : title; + + if (linkTo) { + result.push({ route: linkTo, title: tempTitle }); + + return; + } + } + }); + + return result; +}; diff --git a/ui-next/src/components/searchWrapper/state/hook.ts b/ui-next/src/components/searchWrapper/state/hook.ts new file mode 100644 index 0000000000..c3236be271 --- /dev/null +++ b/ui-next/src/components/searchWrapper/state/hook.ts @@ -0,0 +1,128 @@ +import { useMachine } from "@xstate/react"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { pluginRegistry } from "plugins/registry"; +import { useAuthHeaders } from "utils/query"; +import { + HookActions, + HookState, + SearchActionTypes, + SearchResultItem, +} from "./types"; +import { searchResultExtractor } from "./helpers"; +import { searchMachine } from "./machine"; + +export const useSearchMachine = (): [HookState, HookActions] => { + const authHeaders = useAuthHeaders(); + const [state, send] = useMachine(searchMachine, { + ...(process.env.NODE_ENV === "development" ? { devTools: true } : {}), + context: { + authHeaders, + }, + }); + + const searchTerm = state.context.searchTerm; + const maxSearchResults = state.context.maxSearchResults; + + const setSearchTerm = useCallback( + (searchTerm: string, count?: number) => + send({ + type: SearchActionTypes.UPDATE_SEARCH_TERM, + searchTerm, + count, + }), + [send], + ); + + // Core OSS data from machine context + const taskDefinitions = state.context.taskDefinitions; + const workflowDefinitions = state.context.workflowDefinitions; + const scheduler = state.context.schedulers; + const events = state.context.events; + + // Fetch data from plugin-registered search providers + const [pluginData, setPluginData] = useState>({}); + + useEffect(() => { + const providers = pluginRegistry.getSearchProviders(); + if (providers.length === 0) return; + + let cancelled = false; + + Promise.all( + providers.map(async (provider) => { + try { + const data = await provider.fetcher(authHeaders); + return { id: provider.id, data }; + } catch { + return { id: provider.id, data: [] }; + } + }), + ).then((results) => { + if (cancelled) return; + const dataMap: Record = {}; + for (const { id, data } of results) { + dataMap[id] = data; + } + setPluginData(dataMap); + }); + + return () => { + cancelled = true; + }; + // authHeaders identity is stable across renders so this is safe + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Compute search results combining core + plugin data + const searchResults = useMemo(() => { + // Core OSS search results + const coreResults = searchResultExtractor({ + taskDefinitions, + workflowDefinitions, + scheduler, + events, + searchTerm, + maxSearchResults, + }); + + // Plugin search results + const providers = pluginRegistry.getSearchProviders(); + const pluginResults: SearchResultItem[] = []; + + for (const provider of providers) { + const data = pluginData[provider.id] ?? []; + if (data.length > 0 || searchTerm !== "") { + const mapped = provider.mapper(data, searchTerm) as SearchResultItem[]; + pluginResults.push(...mapped); + } + } + + // If no search term, return null (same as before) + if (searchTerm === "") { + return null; + } + + // Merge and sort by number of sub-results + const combined = [...(coreResults ?? []), ...pluginResults]; + return combined.sort( + ({ sub: subA }, { sub: subB }) => + (subB?.length ?? 0) - (subA?.length ?? 0), + ) as typeof coreResults; + }, [ + taskDefinitions, + workflowDefinitions, + scheduler, + events, + searchTerm, + maxSearchResults, + pluginData, + ]); + + return [ + { + searchTerm, + searchResults, + }, + { setSearchTerm }, + ]; +}; diff --git a/ui-next/src/components/searchWrapper/state/index.ts b/ui-next/src/components/searchWrapper/state/index.ts new file mode 100644 index 0000000000..9eff333659 --- /dev/null +++ b/ui-next/src/components/searchWrapper/state/index.ts @@ -0,0 +1,3 @@ +export * from "./machine"; +export * from "./types"; +export * from "./hook"; diff --git a/ui-next/src/components/searchWrapper/state/machine.ts b/ui-next/src/components/searchWrapper/state/machine.ts new file mode 100644 index 0000000000..03747c433f --- /dev/null +++ b/ui-next/src/components/searchWrapper/state/machine.ts @@ -0,0 +1,109 @@ +import { createMachine } from "xstate"; +import { + SearchActionTypes, + SearchMachineContext, + SearchMachineStates, +} from "./types"; + +import * as services from "./services"; +import * as actions from "./actions"; + +export const searchMachine = createMachine( + { + id: "searchMachine", + initial: SearchMachineStates.INIT, + predictableActionArguments: true, + context: { + authHeaders: undefined, + // Core OSS searchable data + taskDefinitions: [], + workflowDefinitions: [], + schedulers: [], + events: [], + // Plugin-contributed data (populated via hook, not machine) + pluginData: {}, + searchTerm: "", + maxSearchResults: undefined, + }, + states: { + [SearchMachineStates.INIT]: { + type: "parallel", + states: { + [SearchMachineStates.FETCHER]: { + type: "parallel", + states: { + // Core OSS fetchers only + [SearchMachineStates.FETCH_TASK_DEFINITIONS]: { + invoke: { + src: "fetchForTaskNames", + onDone: { + actions: ["persistTaskNames"], + }, + onError: { + actions: ["persistErrorMessage"], + }, + }, + }, + [SearchMachineStates.FETCH_WF_DEFINITIONS]: { + invoke: { + src: "fetchForWorkflowDef", + onDone: { + actions: ["persistWorkflowNames"], + }, + onError: { + actions: ["persistErrorMessage"], + }, + }, + }, + [SearchMachineStates.FETCH_SCHEDULERS]: { + invoke: { + src: "fetchForScheduleNames", + onDone: { + actions: ["persistScheduleNames"], + }, + onError: { + actions: ["persistErrorMessage"], + }, + }, + }, + [SearchMachineStates.FETCH_EVENTS]: { + invoke: { + src: "fetchForEventNames", + onDone: { + actions: ["persistEventNames"], + }, + onError: { + actions: ["persistErrorMessage"], + }, + }, + }, + }, + }, + [SearchMachineStates.FILTER]: { + initial: SearchMachineStates.WAIT, + states: { + [SearchMachineStates.WAIT]: { + after: { + 200: { + target: SearchMachineStates.FILTERING, + }, + }, + }, + [SearchMachineStates.FILTERING]: { + on: { + [SearchActionTypes.UPDATE_SEARCH_TERM]: { + actions: ["persistSearchTerm"], + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + services: services as any, + actions: actions as any, + }, +); diff --git a/ui-next/src/components/searchWrapper/state/services.ts b/ui-next/src/components/searchWrapper/state/services.ts new file mode 100644 index 0000000000..6956ffa88e --- /dev/null +++ b/ui-next/src/components/searchWrapper/state/services.ts @@ -0,0 +1,105 @@ +/** + * Core OSS search service fetchers. + * + * These fetch workflow definitions, task definitions, schedulers, and event + * handlers — all of which are core OSS features. + * + * Enterprise search categories (users, groups, applications, webhooks, + * integrations, prompts, user forms) are registered by enterprise plugins + * via the plugin registry's searchProviders mechanism. + */ + +import { queryClient } from "queryClient"; +import { fetchWithContext, fetchContextNonHook } from "plugins/fetch"; +import _uniq from "lodash/uniq"; +import _isEmpty from "lodash/isEmpty"; + +import { SearchMachineContext } from "./types"; + +const fetchContext = fetchContextNonHook(); + +const ACCESS = "READ"; + +export const fetchForTaskNames = async ({ + authHeaders: headers, + taskDefinitions, +}: SearchMachineContext) => { + if (!_isEmpty(taskDefinitions)) { + return taskDefinitions; + } + + const path = `/metadata/taskdefs?access=${ACCESS}`; + try { + const response = await queryClient.fetchQuery( + [fetchContext.stack, path], + () => fetchWithContext(path, fetchContext, { headers }), + ); + return _uniq( + response.map(({ name, description }: any) => { + return { name, description }; + }), + ).sort(); + } catch { + return Promise.reject("Error fetching tasks "); + } +}; + +export const fetchForWorkflowDef = async ({ + authHeaders: headers, + workflowDefinitions, +}: SearchMachineContext) => { + if (!_isEmpty(workflowDefinitions)) { + return workflowDefinitions; + } + + const path = `/metadata/workflow?short=true&access=${ACCESS}`; + try { + const response = await queryClient.fetchQuery( + [fetchContext.stack, path], + () => fetchWithContext(path, fetchContext, { headers }), + ); + return _uniq(response).sort(); + } catch { + return Promise.reject("Error fetching workflows "); + } +}; + +export const fetchForScheduleNames = async ({ + authHeaders: headers, + schedulers, +}: SearchMachineContext) => { + if (!_isEmpty(schedulers)) { + return schedulers; + } + + const path = `/scheduler/schedules`; + try { + const response = await queryClient.fetchQuery( + [fetchContext.stack, path], + () => fetchWithContext(path, fetchContext, { headers }), + ); + return _uniq(response.map(({ name }: any) => name)).sort(); + } catch { + return Promise.reject("Error fetching schedules "); + } +}; + +export const fetchForEventNames = async ({ + authHeaders: headers, + events, +}: SearchMachineContext) => { + if (!_isEmpty(events)) { + return events; + } + + const path = `/event`; + try { + const response = await queryClient.fetchQuery( + [fetchContext.stack, path], + () => fetchWithContext(path, fetchContext, { headers }), + ); + return _uniq(response.map(({ name }: any) => name)).sort(); + } catch { + return Promise.reject("Error fetching events "); + } +}; diff --git a/ui-next/src/components/searchWrapper/state/types.ts b/ui-next/src/components/searchWrapper/state/types.ts new file mode 100644 index 0000000000..22e3727285 --- /dev/null +++ b/ui-next/src/components/searchWrapper/state/types.ts @@ -0,0 +1,82 @@ +import { ReactElement } from "react"; +import { AuthHeaders, WorkflowDef } from "types"; + +export type SearchResultBase = { + icon?: ReactElement; + title: string; + route?: string; +}; + +type SearchResultRoute = SearchResultBase & { + sub?: never; +}; + +type SearchResultSub = SearchResultBase & { + sub: SearchResults; +}; + +export type SearchResultItem = SearchResultRoute | SearchResultSub; + +export type SearchResults = Array; + +export type CommonDef = { + name: string; + description?: string; + id?: string; + version?: string | number; +}; + +export enum SearchMachineStates { + INIT = "INIT", + FETCHER = "FETCHER", + // Core OSS fetchers + FETCH_TASK_DEFINITIONS = "FETCH_TASK_DEFINITIONS", + FETCH_WF_DEFINITIONS = "FETCH_WF_DEFINITIONS", + FETCH_SCHEDULERS = "FETCH_SCHEDULERS", + FETCH_EVENTS = "FETCH_EVENTS", + // Plugin-provided data + FETCH_PLUGIN_DATA = "FETCH_PLUGIN_DATA", + FILTER = "FILTER", + WAIT = "WAIT", + FILTERING = "FILTERING", +} + +type Error = { + message: string; + severity: string; +}; + +export interface SearchMachineContext { + authHeaders?: AuthHeaders; + // Core OSS searchable data + taskDefinitions: CommonDef[]; + workflowDefinitions: WorkflowDef[]; + schedulers: string[]; + events: string[]; + // Plugin-contributed searchable data: keyed by provider id + pluginData: Record; + searchTerm: string; + error?: Error; + maxSearchResults?: number; +} + +export enum SearchActionTypes { + UPDATE_SEARCH_TERM = "UPDATE_SEARCH_TERM", +} + +export type PersistSearchTermEvent = { + type: SearchActionTypes.UPDATE_SEARCH_TERM; + searchTerm: string; + count?: number; +}; + +export type HookActions = { + setSearchTerm: (value: string, max?: number) => void; +}; + +export type HookState = { + searchTerm: string; + searchResults: SearchResults | null; +}; + +export type SearchMachineEvents = PersistSearchTermEvent; diff --git a/ui-next/src/components/tags/AddTagDialog.tsx b/ui-next/src/components/tags/AddTagDialog.tsx new file mode 100644 index 0000000000..b0c235fed7 --- /dev/null +++ b/ui-next/src/components/tags/AddTagDialog.tsx @@ -0,0 +1,167 @@ +import { + Box, + Dialog, + DialogActions, + DialogContent, + DialogTitle, +} from "@mui/material"; +import ActionButton from "components/ActionButton"; +import MuiAlert from "components/MuiAlert"; +import Button from "components/MuiButton"; +import SaveIcon from "components/v1/icons/SaveIcon"; +import XCloseIcon from "components/v1/icons/XCloseIcon"; +import _differenceWith from "lodash/differenceWith"; +import _uniq from "lodash/uniq"; +import { useState } from "react"; +import { TagDto } from "types/Tag"; +import { useActionWithPath, useTags } from "utils/query"; +import { getErrorMessage } from "utils/utils"; +import ReplaceTagsInput from "./ReplaceTagsInput"; + +export type TagDialogProps = { + open: boolean; + itemName?: string | null; + itemType?: string | null; + onSuccess: () => void; + onClose: () => void; + tags: TagDto[]; + apiPath?: string; +}; + +const parsedTags = (items: string[]): TagDto[] => + items.map((tag: string) => { + const [key, value] = tag.split(":"); + + return { + type: "METADATA", + key, + value, + }; + }); + +const isTagEqual = (tag1: TagDto, tag2: TagDto): boolean => + tag1.key === tag2.key && tag1.value === tag2.value; + +export default function AddTagDialog({ + open, + itemName = null, + itemType = null, + onSuccess, + onClose, + tags = [], + apiPath, +}: TagDialogProps) { + const [errorMessage, setErrorMessage] = useState(null); + const [loading, setLoading] = useState(false); + const [newTags, setNewTags] = useState( + tags.map((tag: TagDto) => tag && `${tag.key}:${tag.value}`), + ); + // Only fetch all tags when the dialog is open (avoids slow /metadata/tags on every page that mounts this dialog). + const { data: existingTags } = useTags({ enabled: open }); + + const replaceTagsAction = useActionWithPath({ + onMutate: () => setLoading(true), + onSuccess: () => { + setErrorMessage(null); + setLoading(false); + onSuccess(); + }, + onError: async (response: Response) => { + setLoading(false); + + const message = await getErrorMessage(response); + setErrorMessage(message || "Error while updating tags."); + }, + retry: 3, + }); + + const hasNoChanges = + _differenceWith(tags, parsedTags(newTags), isTagEqual).length === 0 && + newTags.length === tags.length; + + function replaceTags(newTags: any) { + for (const tag of newTags) { + const tagValue = tag?.inputValue ? tag.inputValue : tag; + + if (tagValue.indexOf(":") < 0 || tagValue.split(":").length !== 2) { + setErrorMessage( + "Invalid tag format. Please review your tags and try again.", + ); + return; + } + } + + // @ts-ignore + replaceTagsAction.mutate({ + method: "PUT", + path: apiPath + ? apiPath + : `/metadata/${itemType}/${encodeURIComponent(itemName ?? "")}/tags`, + body: JSON.stringify(parsedTags(newTags)), + }); + } + + return ( + + Edit Tags + + {errorMessage && ( + + {errorMessage} + + )} + + + Editing tags for ${itemName}. + + } + tags={tags} + options={_differenceWith( + existingTags, + parsedTags(newTags), + isTagEqual, + )} + onChange={(tags) => { + setNewTags(_uniq(tags)); + }} + /> + + + + + replaceTags(newTags)} + startIcon={} + > + Save + + + + ); +} diff --git a/ui-next/src/components/tags/ReplaceTagsInput.tsx b/ui-next/src/components/tags/ReplaceTagsInput.tsx new file mode 100644 index 0000000000..7a51a770a9 --- /dev/null +++ b/ui-next/src/components/tags/ReplaceTagsInput.tsx @@ -0,0 +1,126 @@ +import { Autocomplete } from "@mui/material"; +import { createFilterOptions } from "@mui/material/Autocomplete"; +import TagChip from "components/TagChip"; +import ConductorInput from "components/v1/ConductorInput"; +import XCloseIcon from "components/v1/icons/XCloseIcon"; +import { ReactNode } from "react"; +import { autocompleteStyle } from "shared/styles"; +import { TagDto } from "types/Tag"; + +type ReplaceTagsInputProps = { + onChange?: (tags: string[]) => void | null; + label?: ReactNode; + tags: TagDto[]; + options: TagDto[]; +}; +type SuggestValueType = { title: string; inputValue: string }; + +const filter = createFilterOptions(); + +const ReplaceTagsInput = ({ + label = "Tags", + onChange = () => null, + tags = [], + options = [], +}: ReplaceTagsInputProps) => { + return ( + { + return ( + tag && { + inputValue: `${tag.key}:${tag.value}`, + title: `${tag.key}:${tag.value}`, + } + ); + })} + defaultValue={tags.map((tag: TagDto) => { + return ( + tag && { + inputValue: `${tag.key}:${tag.value}`, + title: `${tag.key}:${tag.value}`, + } + ); + })} + renderTags={(value, getTagProps) => + value.map((option, index) => { + const label = ( + option?.inputValue ? option.inputValue : option + ) as string; + const { key, ...otherTagProps } = getTagProps({ index }); + return ( + + ); + }) + } + getOptionLabel={(option) => { + // Value selected with enter, right from the input + if (typeof option === "string") { + return option; + } + // Add "xxx" option created dynamically + if (option.inputValue) { + // Regular option + return option.title; + } + // Regular option + return option.title; + }} + filterOptions={(options, params) => { + const filtered = filter(options, params); + + const { inputValue } = params; + // Suggest the creation of a new value + const isExisting = options.some( + (option) => inputValue === option.title, + ); + + if (inputValue !== "" && !isExisting) { + filtered.push({ + inputValue, + title: `Add "${inputValue}"`, + }); + } + + return filtered; + }} + onChange={(_event, newValue: (string | SuggestValueType)[]) => { + onChange( + newValue.map((val: string | SuggestValueType) => + typeof val === "string" ? val : val.inputValue, + ), + ); + }} + renderInput={(params) => ( + + Type a tag name using the key:value format and + press enter to create new tags. + + } + /> + )} + sx={[autocompleteStyle({ value: tags })]} + clearIcon={} + /> + ); +}; + +export default ReplaceTagsInput; diff --git a/ui-next/src/components/v1/ActionAlert.tsx b/ui-next/src/components/v1/ActionAlert.tsx new file mode 100644 index 0000000000..7682f585c6 --- /dev/null +++ b/ui-next/src/components/v1/ActionAlert.tsx @@ -0,0 +1,77 @@ +import AlertTitle from "@mui/material/AlertTitle"; +import Alert from "@mui/material/Alert"; +import UndoIcon from "@mui/icons-material/Undo"; +import { greyText, lightPurple, purple } from "theme/tokens/colors"; +import MuiTypography from "../MuiTypography"; + +const actionAlertStyle = { + alert: { + background: "white", + boxShadow: "4px 4px 10px 0px rgba(89, 89, 89, 0.41)", + borderRadius: 1.5, + color: "black", + width: "fit-content", + minWidth: "320px", + "& button": { + border: "1px solid", + padding: 0.5, + color: "gray", + }, + "& .MuiSvgIcon-root": { + fontSize: "14px", + }, + }, + icon: { + color: lightPurple, + }, + title: { + fontWeight: "600", + fontSize: "14px", + }, + message: { + color: greyText, + }, +}; + +type ActionAlertProps = { + title: string; + message: string; + onConfirm: () => void; + onClose: () => void; +}; + +const ActionAlert = ({ + title, + message, + onConfirm, + onClose, +}: ActionAlertProps) => { + return ( + } + sx={actionAlertStyle.alert} + > + {title} + + {message || ( + <> + Want to remove that last action? Just{" "} + + confirm to undo + {" "} + here. + + )} + + + ); +}; + +export type { ActionAlertProps }; +export default ActionAlert; diff --git a/ui-next/src/components/v1/AdvancedSearchFieldPopper.tsx b/ui-next/src/components/v1/AdvancedSearchFieldPopper.tsx new file mode 100644 index 0000000000..75810608b4 --- /dev/null +++ b/ui-next/src/components/v1/AdvancedSearchFieldPopper.tsx @@ -0,0 +1,211 @@ +import { Box, IconButton, Popper, PopperProps, TextField } from "@mui/material"; + +import { colors } from "theme/tokens/variables"; +import { ChangeEvent, useRef } from "react"; +import XCloseIcon from "./icons/XCloseIcon"; +import ArrowUpIcon from "./icons/ArrowUpIcon"; +import ArrowDownIcon from "./icons/ArrowDownIcon"; +import useArrowNavigation from "useArrowNavigation"; +import EnterIcon from "./icons/EnterIcon"; + +type OptionsProps = { + taskName: string; + taskRef: string; + type: string; +}; + +export type AdvancedSearchFieldPopperProps = PopperProps & { + open?: boolean; + options: OptionsProps[]; + handleClose: () => void; + onSelectItem: (value: string | null) => void; + filteredOptionsCount: number; + setFilteredOptionsCount: (count: number) => void; + hoveredItem: string; + setHoveredItem: (item: string) => void; + searchTerm: string; + setSearchTerm: (value: string) => void; + totalOptionsCount: number; +}; + +const SEARCH_POPPER_WIDTH = "370px"; +const SEARCH_DROPDOWN_HEIGHT = "300px"; + +const style = { + inputStyle: { + "& .MuiOutlinedInput-notchedOutline, & .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline": + { + border: "none", + }, + "& .MuiTextField-root, & .MuiInputBase-root ": { + fontSize: "12px", + minHeight: "30px", + borderRadius: "20px", + padding: "0px", + }, + "& .MuiInputAdornment-positionStart": { + width: "12px", + paddingLeft: "8px", + }, + "& .MuiInputAdornment-positionEnd": { + paddingRight: "8px", + }, + }, +}; + +export const AdvancedSearchFieldPopper = ({ + options, + anchorEl, + onSelectItem, + filteredOptionsCount, + setFilteredOptionsCount, + hoveredItem, + setHoveredItem, + searchTerm, + setSearchTerm, + totalOptionsCount, +}: AdvancedSearchFieldPopperProps) => { + const parentPopperRef = useRef(null); + const inputRef = useRef(null); + + const handleInputChange = ( + event: ChangeEvent, + ) => { + setSearchTerm(event.target.value); + const newFilteredOptions = options.filter((option) => + `${option.taskName}${option.taskRef}${option.type}` + .toLowerCase() + .includes(event.target.value.toLowerCase()), + ); + setFilteredOptionsCount(newFilteredOptions.length); + }; + + const uniqueIdGenerator = (data: OptionsProps) => { + return `${data.taskName}${data.taskRef}${data.type}`; + }; + + const { inputProps, optionPropsForItem, moveDown, moveUp } = + useArrowNavigation({ + onSelect: (elem) => { + onSelectItem(elem.taskRef); + }, + options: options || [], + optionsIdGen: uniqueIdGenerator, + scrollToCenter: true, + hoveredItem, + setHoveredItem, + }); + + const handleClearSearch = () => { + setSearchTerm(""); + if (inputRef?.current) { + inputRef.current.focus(); + } + }; + + return ( + + + + {searchTerm && ( + + + + )} + + {filteredOptionsCount}/{totalOptionsCount} + + + + + + + + + + {searchTerm && + options && + options.length > 0 && + options.map((option) => ( + onSelectItem(option.taskRef)} + > + + {option.taskName} + {option.taskRef} + + {option.type} + + {uniqueIdGenerator(option) === hoveredItem && ( + + + + )} + + + ))} + + + ); +}; diff --git a/ui-next/src/components/v1/ApiSearchModal/ApiSearchModal.tsx b/ui-next/src/components/v1/ApiSearchModal/ApiSearchModal.tsx new file mode 100644 index 0000000000..42979e60a6 --- /dev/null +++ b/ui-next/src/components/v1/ApiSearchModal/ApiSearchModal.tsx @@ -0,0 +1,183 @@ +import { Editor } from "@monaco-editor/react"; +import { + CheckCircleOutlined as CheckCircleOutlinedIcon, + Close, + Code as CodeIcon, + FileCopyOutlined as FileCopyOutlinedIcon, +} from "@mui/icons-material"; +import { + Box, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Paper, + Tab, + Tabs, +} from "@mui/material"; +import MuiButton from "components/MuiButton"; +import MuiTypography from "components/MuiTypography"; +import { SnackbarMessage } from "components/SnackbarMessage"; +import { Suspense, SyntheticEvent, useState } from "react"; +import { defaultEditorOptions, type EditorOptions } from "shared/editor"; +import { greyText } from "theme/tokens/colors"; +import { + ApiSearchModalProps, + SupportedDisplayTypes, +} from "../../../shared/CodeModal/types"; +import { modalStyles } from "../Modal/commonStyles"; + +const editorOption: EditorOptions = { + ...defaultEditorOptions, + tabSize: 2, + minimap: { enabled: false }, + quickSuggestions: true, + scrollbar: { + vertical: "hidden", + }, + formatOnType: true, + readOnly: true, + wordWrap: "on", +}; + +const ApiSearchModal = ({ + dialogTitle = "API Search", + dialogHeaderText = "Here is the code for the search parameters that you selected.", + code, + handleClose, + displayLanguage, + onTabChange, + languages, +}: ApiSearchModalProps) => { + const onClose = ( + _event: Event, + reason: "backdropClick" | "escapeKeyDown" | "closeButtonClick", + ) => { + if (reason === "backdropClick") { + return false; + } + handleClose(); + }; + + const [showAlert, setShowAlert] = useState(false); + + const handleChangeTab = ( + _event: SyntheticEvent, + newValue: SupportedDisplayTypes, + ) => { + onTabChange(newValue); + }; + + const handleCopy = () => { + if (code) { + setShowAlert(true); + navigator.clipboard.writeText(code); + } + }; + + return ( + <> + {showAlert && ( + setShowAlert(false)} + anchorOrigin={{ horizontal: "right", vertical: "bottom" }} + /> + )} + + + + {dialogTitle} + + + + + {dialogHeaderText} + + + {languages.map((language) => { + return ( + + + {language} + +
    + } + value={language} + /> + ); + })} + + + Loading...
    }> + + + + + + } + > + Copy + + } + onClick={handleClose} + > + Done + + + + + ); +}; +export { ApiSearchModal }; +export type { ApiSearchModalProps }; diff --git a/ui-next/src/components/v1/ApiSearchModal/index.ts b/ui-next/src/components/v1/ApiSearchModal/index.ts new file mode 100644 index 0000000000..bdd8682aee --- /dev/null +++ b/ui-next/src/components/v1/ApiSearchModal/index.ts @@ -0,0 +1 @@ +export * from "./ApiSearchModal"; diff --git a/ui-next/src/components/v1/ArrowBox.tsx b/ui-next/src/components/v1/ArrowBox.tsx new file mode 100644 index 0000000000..5433ada08a --- /dev/null +++ b/ui-next/src/components/v1/ArrowBox.tsx @@ -0,0 +1,77 @@ +import { Box } from "@mui/material"; + +type ArrowBoxProps = { + children: any; + position?: string; + backgroundColor?: string; + borderColor?: string; +}; + +type ArrowStyleProps = { + position?: string; + backgroundColor: string; + borderColor: string; +}; + +const arrowBoxStyle = ({ backgroundColor, borderColor }: ArrowStyleProps) => { + return { + borderRadius: "6px", + padding: "9px 10px", + fontSize: "12px", + border: `1px solid ${borderColor}`, + background: backgroundColor, + color: "#060606", + position: "relative", + }; +}; + +const arrowStyle = ({ + position, + backgroundColor, + borderColor, +}: ArrowStyleProps) => { + return { + width: "20px", + height: "20px", + transform: "rotate(-45deg);", + background: backgroundColor, + position: "absolute", + borderWidth: "0px 0px 1px 1px", + borderStyle: "solid", + borderColor: borderColor, + bottom: -9.5, + ...(position === "right" ? { right: 30 } : { left: 30 }), + }; +}; + +const ArrowBox = ({ + children, + position = "left", + backgroundColor = "#F3F3F3", + borderColor = "#AFAFAF", +}: ArrowBoxProps) => { + return ( + <> + + + {children} + + + + + ); +}; + +export type { ArrowBoxProps }; +export default ArrowBox; diff --git a/ui-next/src/components/v1/CodeBlockInputWrapper.tsx b/ui-next/src/components/v1/CodeBlockInputWrapper.tsx new file mode 100644 index 0000000000..24e299ee98 --- /dev/null +++ b/ui-next/src/components/v1/CodeBlockInputWrapper.tsx @@ -0,0 +1,412 @@ +import { Editor, EditorProps, Monaco, OnMount } from "@monaco-editor/react"; +import { + Box, + BoxProps, + InputLabel, + Stack, + Typography, + useTheme, +} from "@mui/material"; +import { Theme } from "@mui/material/styles"; +import { SxProps } from "@mui/system"; +import { ArrowsInSimple } from "@phosphor-icons/react"; +import IconButton from "components/MuiIconButton"; +import MuiTypography from "components/MuiTypography"; +import { SnackbarMessage } from "components/SnackbarMessage"; +import { MaybeTooltipLabel } from "components/v1/ConductorInput"; +import { labelScale } from "components/v1/theme/styles"; +import CopyIcon from "components/v1/icons/CopyIcon"; +import { ConductorTooltipProps } from "components/conductorTooltip/ConductorTooltip"; +import _isEmpty from "lodash/isEmpty"; +import { + ReactNode, + RefObject, + useCallback, + useContext, + useRef, + useState, +} from "react"; +import { + editor, + defaultEditorOptions, + type EditorOptions, +} from "shared/editor"; +import { ColorModeContext } from "theme/material/ColorModeContext"; +import { colors, fontSizes } from "theme/tokens/variables"; +import { logger } from "utils/logger"; +import ExpandIcon from "./icons/ExpandIcon"; +import { inputLabelStyle } from "./theme/styles"; +import { getColor } from "./theme/theme"; + +export interface CodeBlockInputWrapperHandle { + handleCopyValue: () => boolean; +} + +const A_MARGIN_THREASHHOLD = 22; +const IDLE_MINIMUM_VALUE_IF_FAIL_TO_GET_REF = 500; + +const DEFAULT_CONTAINER_PROPS = {}; +const DEFAULT_CONTAINER_STYLES = {}; +const DEFAULT_OPTIONS = {}; +const DEFAULT_EDITOR_PROPS = {}; + +const MaybeLabel = ({ + label, + required, + tooltip, +}: { + label?: ReactNode; + required?: boolean; + tooltip?: Omit; +}) => ( + +); + +const smallEditorOptions: EditorOptions = { + ...defaultEditorOptions, + tabSize: 2, + minimap: { enabled: false }, + lightbulb: { enabled: editor.ShowLightbulbIconMode.On }, + quickSuggestions: true, + lineNumbers: "on", + glyphMargin: false, + folding: false, + // Undocumented see https://github.com/Microsoft/vscode/issues/30795#issuecomment-410998882 + lineDecorationsWidth: 10, + lineNumbersMinChars: 0, + renderLineHighlight: "none", + overviewRulerLanes: 0, + hideCursorInOverviewRuler: false, + scrollbar: { + vertical: "hidden", + // this property is added because it was not allowing us to scroll when mouse pointer is over this component + alwaysConsumeMouseWheel: false, + }, + overviewRulerBorder: false, + automaticLayout: true, // Important + scrollBeyondLastLine: false, + wrappingStrategy: "advanced", + wordWrap: "on", +}; + +interface CodeBlockInputWrapperProps { + containerProps?: BoxProps; + containerStyles?: SxProps; + label?: ReactNode; + language?: string; + languageLabel?: string; + error?: boolean; + value?: string; + minHeight: number; + disabled?: boolean; + required?: boolean; + tooltip?: Omit; + enableCopy?: boolean; + onChange?: (value: string) => void; + onMount?: OnMount; + autoformat: boolean; + autoFocus: boolean; + options?: EditorProps["options"]; + editorProps?: Partial; + helperText?: string; + onExpand?: () => void; + isExpanded?: boolean; + showLangLabel: boolean; +} + +const handleUpdateHeight = ( + editor: Monaco, + boxRef: RefObject, + minHeight: number, +) => { + const parentComponent = boxRef?.current; + if (!parentComponent) return; + + const contentHeight = Math.max(minHeight, editor.getContentHeight()); + let contentWidth = IDLE_MINIMUM_VALUE_IF_FAIL_TO_GET_REF; + + if (parentComponent) { + contentWidth = parentComponent.offsetWidth; + const editorSectionElement = parentComponent.querySelector("section"); + + if (editorSectionElement) { + editorSectionElement.style.height = "100%"; + } + } + + try { + editor.layout({ + width: contentWidth - A_MARGIN_THREASHHOLD, + height: contentHeight, + }); + } catch (error) { + logger.error("[handleEditorDidMount]: error", error); + } +}; + +export const CodeBlockInputWrapper = ({ + containerProps = DEFAULT_CONTAINER_PROPS, + containerStyles = DEFAULT_CONTAINER_STYLES, + label, + language = "json", + languageLabel, + error = false, + value = "", + minHeight, + disabled = false, + required = false, + tooltip, + enableCopy = true, + onChange, + onMount, + autoformat = true, + autoFocus = false, + options = DEFAULT_OPTIONS, + editorProps = DEFAULT_EDITOR_PROPS, + helperText, + onExpand, + isExpanded = false, + showLangLabel, +}: CodeBlockInputWrapperProps) => { + const theme = useTheme(); + const { mode } = useContext(ColorModeContext); + const [isFocused, setIsFocused] = useState(false); + const [showCopyAlert, setShowCopyAlert] = useState(false); + + const boxRef = useRef(null); + const editorRef = useRef(null); + + const handleEditorDidMount = useCallback( + (editor: Monaco, monaco?: unknown) => { + editorRef.current = editor; + + if (onMount) { + onMount(editor, monaco); + } + + if (autoformat) { + editor.onDidBlurEditorWidget(() => { + editor.getAction("editor.action.formatDocument").run(); + }); + } + + if (autoFocus) { + editor.focus(); + setIsFocused(true); + } + const updateHeight = () => handleUpdateHeight(editor, boxRef, minHeight); + + editor.onDidContentSizeChange(updateHeight); + updateHeight(); + + editor.onDidFocusEditorText(() => setIsFocused(true)); + editor.onDidBlurEditorText(() => setIsFocused(false)); + }, + [onMount, autoformat, autoFocus, minHeight], + ); + + const handleCopyValue = () => { + const editorValue = editorRef?.current?.getValue(); + if (editorValue) { + setShowCopyAlert(true); + navigator.clipboard.writeText(editorValue); + } + }; + + const handleEditorChange = useCallback(() => { + const editorValue = editorRef?.current?.getValue(); + onChange?.(editorValue); + }, [onChange]); + + return ( + <> + {showCopyAlert && ( + setShowCopyAlert(false)} + anchorOrigin={{ horizontal: "right", vertical: "bottom" }} + /> + )} + + section": { + mt: "-6px", + pl: "8px", + borderRadius: "4px", + resize: "vertical", + overflow: "visible", + minHeight: `${minHeight}px`, + }, + + ".monaco-editor": { + ".scroll-decoration": { + boxShadow: "none", + }, + ".suggest-widget": { + zIndex: 99999, + }, + }, + ...containerStyles, + }} + > + {label && ( + <> +
    + + + +
    + + + + + + )} + {showLangLabel && ( + + + {languageLabel + ? languageLabel.toUpperCase() + : language.toUpperCase()} + + + )} + + + {isExpanded ? : } + + {enableCopy && ( + + + + )} + + +
    + + {helperText && ( + (error ? theme.palette.input.error : "unset"), + }} + > + {helperText} + + )} + + ); +}; diff --git a/ui-next/src/components/v1/CodeSnippet.tsx b/ui-next/src/components/v1/CodeSnippet.tsx new file mode 100644 index 0000000000..39ec7c76c2 --- /dev/null +++ b/ui-next/src/components/v1/CodeSnippet.tsx @@ -0,0 +1,57 @@ +import { useState } from "react"; +import { Box, Button, Stack } from "@mui/material"; +import Highlight from "react-highlight"; + +export const CodeSnippet = ({ + code, + className, + noCopyToClipboard, +}: { + code: string; + className?: string; + noCopyToClipboard?: boolean; + sx?: any; +}) => { + const [buttonText, setButtonText] = useState("Copy"); + + const handleCopy = () => { + navigator.clipboard.writeText(code); + setButtonText("Copied!"); + setTimeout(() => { + setButtonText("Copy"); + }, 1000); + }; + + return ( + *": { whiteSpace: "pre-wrap", overflowWrap: "anywhere" }, + }} + > + {code} + + {!noCopyToClipboard && ( + + + + )} + + ); +}; diff --git a/ui-next/src/components/v1/ConductorArrayField.tsx b/ui-next/src/components/v1/ConductorArrayField.tsx new file mode 100644 index 0000000000..f3abab7e6b --- /dev/null +++ b/ui-next/src/components/v1/ConductorArrayField.tsx @@ -0,0 +1,223 @@ +import { Box, Grid, MenuItem } from "@mui/material"; +import Button from "components/MuiButton"; +import IconButton from "components/MuiIconButton"; +import _isEmpty from "lodash/isEmpty"; +import _isNull from "lodash/isNull"; +import FieldTypeDropdown from "pages/definition/EditorPanel/TaskFormTab/forms/FieldTypeDropdown"; +import maybeVariable from "pages/definition/EditorPanel/TaskFormTab/forms/maybeVariableHOC"; +import { FunctionComponent, ReactNode } from "react"; +import { FieldType } from "types/common"; +import { adjust, remove } from "utils/array"; +import { ButtonPosition } from "utils/constants/common"; +import { castToType } from "utils/helpers"; +import { ConductorEmptyGroupField } from "./ConductorEmptyGroupField"; +import ConductorInput from "./ConductorInput"; +import ConductorSelect from "./ConductorSelect"; +import { ConductorAutocompleteVariables } from "./FlatMapForm/ConductorAutocompleteVariables"; +import AddIcon from "./icons/AddIcon"; +import TrashIcon from "./icons/TrashIcon"; + +interface RemovableFieldProps { + onChange: (value: any) => void; + value?: string; + onRemove?: () => void; + isError?: boolean; + hasAtLeastOne?: boolean; + placeholder?: string; + customInput?: ReactNode; + inputLabel?: ReactNode; + showType?: boolean; + addButtonPosition?: ButtonPosition; + helperText?: ReactNode; + typeLabel?: ReactNode; +} + +const MaybeInput = ({ + customInput, + inputLabel, + value, + placeholder, + isError, + helperText, + onChange, +}: RemovableFieldProps) => { + const trimmedValue = typeof value === "string" ? value.trim() : value; + const isEmptyValue = _isEmpty(trimmedValue); + const isNullValue = _isNull(value); + + return customInput ? ( + customInput + ) : ( + onChange(val)} + disabled={isNullValue} + error={isError && isEmptyValue} + helperText={isEmptyValue && helperText} + /> + ); +}; + +const RemovableField: FunctionComponent = (props) => { + const { onChange, value, onRemove, showType, typeLabel, inputLabel } = props; + + return ( + + + {showType && ( + + + onChange(castToType(value, type)) + } + hideObjectArray + /> + + )} + + + {typeof value === "boolean" ? ( + { + onChange(ev.target.value === "true"); + }} + > + True + False + + ) : value === null ? ( + <> + ) : ( + + )} + + + + {onRemove && ( + onRemove!()} + style={{ paddingTop: "0.42em" }} + > + + + )} + + + ); +}; + +export interface ConductorArrayFieldProps { + value: string[]; + onChange: (val: string[]) => void; + isError?: boolean; + placeholder?: string; + customInput?: ReactNode; + addButtonLabel?: string; + inputLabel?: ReactNode; + showType?: boolean; + addButtonPosition?: ButtonPosition; + disabledAddButton?: boolean; + enableAutocomplete?: boolean; + typeLabel?: ReactNode; + helperText?: ReactNode; +} + +const ConductorArrayFieldBase: FunctionComponent = ({ + value = [], + onChange, + isError, + placeholder, + customInput, + addButtonLabel = "Add", + inputLabel = "Value", + typeLabel = "Type", + showType, + addButtonPosition, + disabledAddButton, + enableAutocomplete, + helperText, +}) => { + const handleValueChange = (index: number) => (newValue: string) => { + onChange(adjust(index, () => newValue, value)); + }; + + const handleRemoveValue = (index: number) => () => { + onChange(remove(index, 1, value)); + }; + + const handleAddItem = () => onChange(value.concat("")); + + return value.length === 0 ? ( + + ) : ( + <> + + + {value.map((val, index) => ( + + ) : ( + customInput + ) + } + inputLabel={inputLabel} + typeLabel={typeLabel} + showType={showType} + addButtonPosition={addButtonPosition} + helperText={helperText} + /> + ))} + + + + + ); +}; + +const ConductorArrayField = maybeVariable(ConductorArrayFieldBase); +export { ConductorArrayField, ConductorArrayFieldBase }; diff --git a/ui-next/src/components/v1/ConductorAutoComplete.tsx b/ui-next/src/components/v1/ConductorAutoComplete.tsx new file mode 100644 index 0000000000..1cd960609e --- /dev/null +++ b/ui-next/src/components/v1/ConductorAutoComplete.tsx @@ -0,0 +1,159 @@ +import CancelOutlinedIcon from "@mui/icons-material/CancelOutlined"; +import { + Autocomplete, + AutocompleteProps, + AutocompleteRenderGetTagProps, + Chip, + Popper, +} from "@mui/material"; +import match from "autosuggest-highlight/match"; +import parse from "autosuggest-highlight/parse"; +import { forwardRef } from "react"; +import { autocompleteStyle } from "shared/styles"; +import ConductorInput, { ConductorInputProps } from "./ConductorInput"; +import XCloseIcon from "./icons/XCloseIcon"; + +export type ConductorAutocompleteProps = Omit< + AutocompleteProps< + T, + boolean | undefined, + boolean | undefined, + boolean | undefined + >, + "renderInput" +> & { + label: string; + placeholder?: string; + error?: boolean; + required?: boolean; + helperText?: string; + conductorInputProps?: Partial; + id?: string; + onTextInputChange?: (v: string) => void; + dataTestId?: string; +}; + +export const ConductorAutoComplete = forwardRef( + ( + { + id, + options = [], + fullWidth, + disabled, + value, + helperText, + label, + placeholder, + error, + required, + conductorInputProps = {}, + onTextInputChange, + sx, + renderOption, + dataTestId, + ...rest + }: ConductorAutocompleteProps, + ref, + ) => { + return ( + 0 + ? { + ".MuiTextField-root": { + ".MuiOutlinedInput-root": { + pt: "9px", + pl: "2px", + pb: "3px", + }, + }, + } + : null, + ]} + ref={ref} + renderOption={ + renderOption + ? renderOption + : (props, option, { inputValue }) => { + const matches = match(option as string, inputValue); + const parts = parse(option as string, matches); + + const { key, ...otherProps } = props; + return ( +
  • +
    + {parts.map((part, index) => ( + + {rest.getOptionLabel + ? rest.getOptionLabel(part.text) + : part.text} + + ))} +
    +
  • + ); + } + } + autoComplete + renderInput={(params) => ( + + )} + options={options} + fullWidth={fullWidth} + disabled={disabled} + value={value} + PopperComponent={(props) => } + clearIcon={} + renderTags={( + value: string[], + getTagProps: AutocompleteRenderGetTagProps, + ) => + value.map((v: string | { label: string }, index) => { + const renderableLabel: string = + typeof v === "string" || typeof v === "number" ? v : v.label; + const { key, ...otherTagProps } = getTagProps({ index }); + return ( + } + /> + ); + }) + } + {...rest} + /> + ); + }, +); diff --git a/ui-next/src/components/v1/ConductorAutoCompleteWithDescription.tsx b/ui-next/src/components/v1/ConductorAutoCompleteWithDescription.tsx new file mode 100644 index 0000000000..b032a172e6 --- /dev/null +++ b/ui-next/src/components/v1/ConductorAutoCompleteWithDescription.tsx @@ -0,0 +1,116 @@ +import { Popper } from "@mui/material"; +import Autocomplete, { createFilterOptions } from "@mui/material/Autocomplete"; +import Box from "@mui/material/Box"; +import { CSSProperties, FunctionComponent, ReactNode } from "react"; +import { autocompleteStyle } from "shared/styles"; +import { fontWeights } from "theme/tokens/variables"; +import ConductorInput from "./ConductorInput"; +import XCloseIcon from "./icons/XCloseIcon"; + +const filter = createFilterOptions(); + +interface ConductorAutoCompleteWithDescriptionProps { + value?: string; + options?: { name: string; description: ReactNode }[]; + error?: boolean; + helperText?: string; + onChange: (value: string) => void; + placeholder?: string; + growPopper?: boolean; + label?: ReactNode; + disableClearable?: boolean; +} + +export const ConductorAutoCompleteWithDescription: FunctionComponent< + ConductorAutoCompleteWithDescriptionProps +> = ({ + value, + options = [], + error = false, + helperText, + onChange, + placeholder = "", + growPopper, + label, + disableClearable = false, +}) => { + const popperStyle = (style: CSSProperties | undefined) => { + return growPopper ? { maxWidth: "300px" } : style; + }; + return ( + ( + + )} + value={value ? value : ""} + isOptionEqualToValue={(option: any, currentValue: any) => + option?.name === currentValue + } + autoHighlight + componentsProps={{ paper: { elevation: 3 } }} + onChange={(_event, newValue: any) => { + onChange(newValue?.name); + }} + filterOptions={(options, params) => { + const filtered = filter(options, params); + return filtered; + }} + id="assignment-type-dialog" + options={options} + getOptionLabel={(option) => { + // e.g value selected with enter, right from the input + if (typeof option === "string") { + return option; + } + return option?.name; + }} + selectOnFocus + clearOnBlur + handleHomeEndKeys + renderOption={(props, option) => { + const { key, ...otherProps } = props; + return ( +
  • + + + {option.name} + + {option.description} + +
  • + ); + }} + freeSolo + renderInput={(params) => ( + + )} + sx={[autocompleteStyle({ value })]} + clearIcon={} + disableClearable={disableClearable} + /> + ); +}; diff --git a/ui-next/src/components/v1/ConductorBreadcrumbs.tsx b/ui-next/src/components/v1/ConductorBreadcrumbs.tsx new file mode 100644 index 0000000000..4f95768bff --- /dev/null +++ b/ui-next/src/components/v1/ConductorBreadcrumbs.tsx @@ -0,0 +1,66 @@ +import { Breadcrumbs, BreadcrumbsProps, SxProps, Theme } from "@mui/material"; +import Typography from "@mui/material/Typography"; +import { styled } from "@mui/system"; +import { Link } from "react-router"; +import { blue15 } from "theme/tokens/colors"; + +type ConductorBreadcrumbsProps = BreadcrumbsProps & { + items: any; + color?: string; +}; + +const StyledLink = styled(Link)` + text-decoration: none; + color: ${(props) => (props.color ? props.color : blue15)}; + font-size: 12px; + font-weight: 300; + line-height: 16px; + display: "flex", + alignItems: "center", +`; + +const typographyStyle: SxProps = { + fontSize: "12px", + fontWeight: 300, + color: (theme) => theme.palette.input.text, + lineHeight: "16px", + display: "flex", + alignItems: "center", +}; +const globalStyles: SxProps = { + ".MuiBreadcrumbs-separator": { + color: "#161616", + ".MuiSvgIcon-root": { + fontSize: "28px", + }, + }, +}; +const ConductorBreadcrumbs = ({ + items, + color, + ...rest +}: ConductorBreadcrumbsProps) => { + return ( + <> + + {items && + items.map((item: any, index: number) => + index !== items.length - 1 ? ( + + {item.label} + {item.icon} + + ) : ( + + {item.label} + {item.icon} + + ), + )} + + + ); +}; + +export type { ConductorBreadcrumbsProps }; +export default ConductorBreadcrumbs; diff --git a/ui-next/src/components/v1/ConductorCheckbox.tsx b/ui-next/src/components/v1/ConductorCheckbox.tsx new file mode 100644 index 0000000000..175b0bc247 --- /dev/null +++ b/ui-next/src/components/v1/ConductorCheckbox.tsx @@ -0,0 +1,31 @@ +import { FormControlLabel } from "@mui/material"; +import MuiCheckbox from "components/MuiCheckbox"; + +export type ConductorCheckboxProps = { + id?: string; + label?: string; + value?: boolean; + onChange?: (value: boolean) => void; +}; + +export const ConductorCheckbox = ({ + id, + label, + value, + onChange, +}: ConductorCheckboxProps) => { + return ( + { + onChange?.(value); + }} + /> + } + label={label} + /> + ); +}; diff --git a/ui-next/src/components/v1/ConductorCodeBlockInput.tsx b/ui-next/src/components/v1/ConductorCodeBlockInput.tsx new file mode 100644 index 0000000000..d5fbfae8f8 --- /dev/null +++ b/ui-next/src/components/v1/ConductorCodeBlockInput.tsx @@ -0,0 +1,131 @@ +import { EditorProps } from "@monaco-editor/react"; +import { + Box, + BoxProps, + Dialog, + DialogContent, + DialogTitle, + IconButton, +} from "@mui/material"; +import { Theme } from "@mui/material/styles"; +import { SxProps } from "@mui/system"; +import { FunctionComponent, ReactNode, useCallback, useState } from "react"; + +import { ConductorTooltipProps } from "components/conductorTooltip/ConductorTooltip"; + +import { CodeBlockInputWrapper } from "./CodeBlockInputWrapper"; +import Close from "@mui/icons-material/Close"; + +export type ConductorCodeBlockInputProps = { + label?: ReactNode; + helperText?: string; + language?: string; + onChange?: (value: string) => void; + value?: string; + containerProps?: BoxProps; + error?: boolean; + height?: number | "auto"; + minHeight?: number; + autoformat?: boolean; + labelStyle?: SxProps; + languageLabel?: string; + containerStyles?: SxProps; + required?: boolean; + disabled?: boolean; + tooltip?: Omit; + enableCopy?: boolean; + autoFocus?: boolean; + showLangLabel?: boolean; +} & Partial>; + +const MIN_HEIGHT = 120; + +export const ConductorCodeBlockInput: FunctionComponent< + ConductorCodeBlockInputProps +> = ({ + label = "Code", + language = "json", + onChange = () => null, + onMount, + value = "", + containerProps = {}, + error = false, + minHeight = MIN_HEIGHT, + autoformat = true, + languageLabel, + containerStyles = {}, + required, + tooltip, + disabled, + enableCopy = true, + options, + helperText, + autoFocus = false, + showLangLabel = true, + ...restOfProps +}) => { + const [isExpanded, setIsExpanded] = useState(false); + + const handleExpandToggle = useCallback(() => { + setIsExpanded(!isExpanded); + }, [isExpanded]); + + const codeBlockWrapper = ( + + ); + + return ( + <> + {isExpanded ? ( + + + Code Editor + setIsExpanded(false)} + sx={{ + position: "absolute", + right: 8, + top: 8, + }} + > + + + + + {codeBlockWrapper} + + + ) : ( + codeBlockWrapper + )} + + ); +}; diff --git a/ui-next/src/components/v1/ConductorEmptyGroupField.tsx b/ui-next/src/components/v1/ConductorEmptyGroupField.tsx new file mode 100644 index 0000000000..245105961a --- /dev/null +++ b/ui-next/src/components/v1/ConductorEmptyGroupField.tsx @@ -0,0 +1,77 @@ +import { + Box, + Button, + CircularProgress, + Stack, + Typography, +} from "@mui/material"; +import { ReactNode } from "react"; + +import AddIcon from "./icons/AddIcon"; + +export const ConductorEmptyGroupField = ({ + addButtonLabel, + handleAddItem, + id, + loading = false, + compact = false, + emptyListMessage, +}: { + addButtonLabel?: ReactNode; + handleAddItem: () => void; + id?: string; + loading?: boolean; + compact?: boolean; + emptyListMessage?: ReactNode; +}) => { + if (compact) { + return ( + + {emptyListMessage && ( + + {emptyListMessage} + + )} + + + + ); + } + return ( + + + + ); +}; diff --git a/ui-next/src/components/v1/ConductorGroupContainer.tsx b/ui-next/src/components/v1/ConductorGroupContainer.tsx new file mode 100644 index 0000000000..b46376b17e --- /dev/null +++ b/ui-next/src/components/v1/ConductorGroupContainer.tsx @@ -0,0 +1,14 @@ +import { Box, BoxProps, GridProps } from "@mui/material"; +import { FC, ReactNode } from "react"; + +export type ConductorGroupContainerProps = { + Wrapper?: FC; + children?: ReactNode; +}; + +export const ConductorGroupContainer = ({ + Wrapper = Box, + children, +}: ConductorGroupContainerProps) => { + return {children}; +}; diff --git a/ui-next/src/components/v1/ConductorGroupFieldTitle.tsx b/ui-next/src/components/v1/ConductorGroupFieldTitle.tsx new file mode 100644 index 0000000000..98e9d6c43b --- /dev/null +++ b/ui-next/src/components/v1/ConductorGroupFieldTitle.tsx @@ -0,0 +1,34 @@ +import { Grid } from "@mui/material"; +import { ReactNode } from "react"; + +import { colors, fontSizes, fontWeights } from "theme/tokens/variables"; + +export const ConductorGroupFieldTitle = ({ title }: { title?: ReactNode }) => { + return ( + + + {title} + + + ); +}; diff --git a/ui-next/src/components/v1/ConductorInput.tsx b/ui-next/src/components/v1/ConductorInput.tsx new file mode 100644 index 0000000000..31156710f8 --- /dev/null +++ b/ui-next/src/components/v1/ConductorInput.tsx @@ -0,0 +1,418 @@ +import ContentCopyOutlinedIcon from "@mui/icons-material/ContentCopyOutlined"; +import VisibilityIcon from "@mui/icons-material/Visibility"; +import VisibilityOffIcon from "@mui/icons-material/VisibilityOff"; +import { + Box, + IconButton, + TextField, + TextFieldProps, + Theme, + useTheme, +} from "@mui/material"; +import InputAdornment from "@mui/material/InputAdornment"; +import ConductorToolTip, { + ConductorTooltipProps, +} from "components/conductorTooltip/ConductorTooltip"; +import _isEmpty from "lodash/isEmpty"; +import { + ChangeEvent, + FocusEvent, + MouseEvent, + ReactNode, + Ref, + forwardRef, + useContext, + useRef, + useState, +} from "react"; +import { colors, fontSizes } from "theme/tokens/variables"; +import InfoIcon from "./icons/InfoIcon"; +import XCloseIcon from "./icons/XCloseIcon"; +import { MessageContext } from "./layout/MessageContext/MessageContext"; +import { formHelperStyle, inputLabelStyle, labelScale } from "./theme/styles"; +import { getColor } from "./theme/theme"; + +export type ConductorInputStyleProps = { + theme: Theme; + isFocused?: boolean; + error?: boolean; + multiline?: boolean; + disabled?: boolean; + isLabel?: boolean; + isInputEmpty?: boolean; +}; + +const inputStyle = ({ + theme, + error, + isFocused, + disabled, +}: ConductorInputStyleProps) => ({ + backgroundColor: colors.white, + fontSize: fontSizes.fontSize3, + fontWeight: 200, + color: error ? theme.palette.input.error : theme.palette.input.text, + minHeight: "unset", + + // Remove autofill background's input + "& input:-webkit-autofill": { + WebkitBoxShadow: "0 0 0 100px #ffffff inset", // Set the box-shadow to none + }, + + "&.MuiOutlinedInput-root": { + ".MuiInputBase-input": { + padding: "14px 8px 8px 8px", + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + "&.Mui-disabled": { + WebkitTextFillColor: theme.palette.input.label, + }, + }, + ".MuiInputBase-inputMultiline": { + whiteSpace: "pre-wrap", + overflow: "auto", + textOverflow: "unset", + p: 0, + }, + ".MuiOutlinedInput-notchedOutline": { + borderWidth: 1, + borderStyle: "solid", + borderRadius: "4px", + borderColor: getColor({ theme, isFocused, error }), + + // This will make the legend has same size with the label + "& legend": { + maxWidth: "100%", + fontSize: `${labelScale}em`, + fontWeight: isFocused ? 500 : "unset", + }, + }, + + "&.Mui-focused.MuiOutlinedInput-notchedOutline": { + borderWidth: 1, + }, + + "&:hover fieldset": disabled + ? null + : { + borderColor: theme.palette.input.focus, + }, + + "&.Mui-focused": { + backgroundColor: disabled ? colors.lightGrey : colors.white, + }, + + "&.Mui-disabled": { + WebkitTextFillColor: theme.palette.input.label, + borderColor: getColor({ theme }), + backgroundColor: colors.lightGrey, + }, + + "&.MuiInputBase-multiline": { + p: "14px 8px 8px 8px", + }, + }, + + "& ::placeholder": { + color: colors.greyText2, + }, + + ".MuiSelect-select": { + "&.MuiInputBase-input": { + pr: "32px", + + "&:focus": { + backgroundColor: disabled ? colors.lightGrey : colors.white, + borderRadius: "4px 0 0 4px", + }, + }, + }, +}); + +export const MaybeTooltipLabel = ({ + tooltip, + label, + required, +}: { + tooltip?: Omit; + label: ReactNode; + required?: boolean; +}) => ( + <> + {tooltip ? ( + + + {label} + + + + {required && label && "*"} + + + ) : ( + label + )} + +); + +const CustomEndAdornment = ({ + clearValue, + disabled, + handleClickShowSecret, + handleCopyValue, + handleMouseDownSecret, + isSecret, + multiline, + showClearButton, + showSecret, + value, +}: { + clearValue: () => void; + disabled?: boolean; + handleClickShowSecret: () => void; + handleCopyValue: () => void; + handleMouseDownSecret: (event: MouseEvent) => void; + isSecret?: boolean; + multiline?: boolean; + showClearButton?: boolean; + showSecret?: boolean; + value: unknown; +}) => ( + + {showClearButton ? ( + + + + ) : null} + + {isSecret && !!value ? ( + + + + ) : null} + + {!multiline && isSecret ? ( + + {showSecret ? : } + + ) : null} + +); + +type ConductorInputProps = Omit & { + onTextInputChange?: (value: string) => void; + isSecret?: boolean; + showClearButton?: boolean; + tooltip?: Omit; +}; + +const ConductorInput = forwardRef( + ( + { + label, + placeholder, + autoFocus, + required, + onBlur, + onChange, + onTextInputChange, + onFocus, + multiline, + isSecret, + fullWidth, // FIXME: just fixed this the prop was not passed down this may affect modals + error, + helperText, + showClearButton, + tooltip, + value, + InputProps, + disabled, + ...rest + }: ConductorInputProps, + ref: Ref, + ) => { + const theme = useTheme(); + const inputRef = useRef(null); + const { setMessage } = useContext(MessageContext); + const [isFocused, setIsFocused] = useState(autoFocus); + const [showSecret, setShowSecret] = useState(false); + + const handleFocus = ( + event: FocusEvent, + ) => { + setIsFocused(true); + + if (onFocus) { + onFocus(event); + } + }; + + const handleBlur = ( + event: FocusEvent, + ) => { + setIsFocused(false); + + if (onBlur) { + onBlur(event); + } + }; + + const handleClickShowSecret = () => { + setShowSecret((show) => !show); + + if (inputRef.current) { + inputRef.current.focus(); + } + }; + + const handleMouseDownSecret = (event: MouseEvent) => { + event.preventDefault(); + }; + + const handleCopyValue = () => { + const currentValue = inputRef.current?.value || ""; + + if (currentValue) { + setMessage({ + text: "Copied to Clipboard", + severity: "success", + }); + navigator.clipboard.writeText(currentValue); + } + }; + + const handleChange = ( + event: ChangeEvent, + ) => { + const { value } = event.target; + + if (onChange) { + onChange(event); + } + + if (onTextInputChange) { + onTextInputChange(value); + } + }; + + const clearValue = () => { + if (onChange) { + onChange({ target: { value: "" } } as ChangeEvent< + HTMLInputElement | HTMLTextAreaElement + >); + } + + if (onTextInputChange) { + onTextInputChange(""); + } + + if (inputRef.current) { + inputRef.current.focus(); + } + }; + + const getCustomEndAdornment = () => { + if (InputProps?.endAdornment) { + return InputProps.endAdornment; + } + + if (showClearButton || isSecret) { + return ( + + ); + } + + return undefined; + }; + + const isInputEmpty = _isEmpty(value) && _isEmpty(rest.defaultValue); + + return ( + + ) : undefined + } + InputLabelProps={{ + sx: inputLabelStyle({ + theme, + isFocused, + isInputEmpty, + error, + disabled, + }), + shrink: true, + }} + InputProps={{ + ...InputProps, + endAdornment: getCustomEndAdornment(), + sx: [ + inputStyle({ + theme, + error, + multiline, + isFocused, + disabled, + }), + ], + }} + helperText={helperText} + FormHelperTextProps={{ + sx: formHelperStyle({ + theme, + isFocused, + isInputEmpty, + error, + }), + }} + {...rest} + /> + ); + }, +); + +export type { ConductorInputProps }; +export default ConductorInput; diff --git a/ui-next/src/components/v1/ConductorInputNumber.tsx b/ui-next/src/components/v1/ConductorInputNumber.tsx new file mode 100644 index 0000000000..1b4d711f24 --- /dev/null +++ b/ui-next/src/components/v1/ConductorInputNumber.tsx @@ -0,0 +1,88 @@ +import { ChangeEvent, forwardRef, FunctionComponent } from "react"; +import { NumericFormat, NumericFormatProps } from "react-number-format"; + +import ConductorInput, { + ConductorInputProps, +} from "components/v1/ConductorInput"; + +export type ConductorInputNumberProps = Omit< + ConductorInputProps, + "onChange" | "onBlur" +> & { + value: number | null; + onChange: ( + val: number | null, + event?: ChangeEvent, + ) => void; +}; + +interface NumericFormatCustomProps { + onChange: (event: { target: { name: string; value: string } }) => void; + name: string; +} + +const NumericFormatCustom = forwardRef< + NumericFormatProps, + NumericFormatCustomProps & { + min?: number; + max?: number; + allowFloat?: boolean; + } +>(function NumericFormatCustom(props, ref) { + const { onChange, min, max, allowFloat = true, ...other } = props; + + return ( + { + const { floatValue } = values; + return ( + floatValue === undefined || + ((min === undefined || floatValue >= min) && + (max === undefined || floatValue <= max)) + ); + }} + onValueChange={(values) => { + onChange({ + target: { + name: props.name, + value: values.value, + }, + }); + }} + /> + ); +}); + +const ConductorInputNumber: FunctionComponent = ({ + label = "", + value, + onChange, + ...restProps +}) => { + const handleChange = ( + event: ChangeEvent, + ) => { + const { value } = event.target; + const returnedValue: number | null = value === "" ? null : Number(value); + + onChange(returnedValue, event); + }; + + return ( + + ); +}; + +export default ConductorInputNumber; diff --git a/ui-next/src/components/v1/ConductorMultiSelect.tsx b/ui-next/src/components/v1/ConductorMultiSelect.tsx new file mode 100644 index 0000000000..f1c6e7c0af --- /dev/null +++ b/ui-next/src/components/v1/ConductorMultiSelect.tsx @@ -0,0 +1,124 @@ +import ListItemText from "@mui/material/ListItemText"; +import MenuItem from "@mui/material/MenuItem"; +import { ReactNode, useEffect, useState } from "react"; + +import MuiCheckbox from "components/MuiCheckbox"; +import ConductorSelect from "./ConductorSelect"; + +const ALL_VALUE = "all"; +const itemHeight = 48; +const itemPaddingTop = 8; +const additionalHeight = 4.5; + +type ConductorMultiSelectProp = { + label: string; + options: string[]; + onSelected: (val: string[]) => void; + allText: string; + value: string[]; + renderer?: (val: string) => ReactNode; + dataTestId?: string; + error?: boolean; + helperText?: string; +}; + +type MenuPropsType = { + PaperProps: { + style: { + maxHeight: number; + width: string; + }; + }; +}; + +export default function ConductorMultiSelect({ + label, + options, + onSelected, + allText, + value = [], + renderer, + dataTestId, + error, + helperText = "", +}: ConductorMultiSelectProp) { + const menuProps: MenuPropsType = { + PaperProps: { + style: { + maxHeight: itemHeight * additionalHeight + itemPaddingTop, + width: "auto", + }, + }, + }; + const [selected, setSelected] = useState(value); + const isAllChecked = options.length > 0 && selected.length === options.length; + const isIndeterminate = + selected.length > 0 && selected.length < options.length; + + const handleChange = (event: any) => { + const { value } = event.target; + + if (value[value.length - 1] === "all") { + setSelected(selected.length === options.length ? [] : options); + return; + } + + setSelected(value); + }; + + const rendersSelectedValues = (selectedValues: string[]) => { + if (isAllChecked || selectedValues.length === 0) { + return allText; + } + + return renderer + ? selectedValues.map((value) => renderer(value)) + : selectedValues.join(", "); + }; + + useEffect(() => { + onSelected(selected.filter((x: string) => allText !== x)); + }, [selected, onSelected, allText]); + + return ( + rendersSelectedValues(value as string[]), + MenuProps: menuProps, + }} + error={error} + helperText={helperText} + sx={[ + // reduce padding top when having value + ![0, 5].includes(value.length) && { + ".MuiInputBase-root": { + ".MuiSelect-select": { + pt: "10px", + ".MuiInputBase-input": { + pt: "10px", + }, + }, + }, + }, + ]} + > + + + + + {options.map((option: string) => ( + + -1} /> + {renderer ? renderer(option) : } + + ))} + + ); +} diff --git a/ui-next/src/components/v1/ConductorNameVersionField.tsx b/ui-next/src/components/v1/ConductorNameVersionField.tsx new file mode 100644 index 0000000000..8ab4318fde --- /dev/null +++ b/ui-next/src/components/v1/ConductorNameVersionField.tsx @@ -0,0 +1,162 @@ +import { Box, MenuItem, Stack } from "@mui/material"; +import _isNil from "lodash/isNil"; +import _last from "lodash/last"; +import { forwardRef, useImperativeHandle, useMemo } from "react"; + +import { ConductorAutoComplete } from "components/v1"; +import ConductorSelect from "components/v1/ConductorSelect"; +import { useFetch } from "utils/query"; + +export interface ConductorNameVersionFieldProps { + label: string; + optionsUrl: string; + value?: { + name: string; + version?: number; + }; + onChange?: (value?: { name?: string; version?: number }) => void; + mapOptions?: (data: any) => { name: string; versions: number[] }[]; + nameField?: { + id?: string; + clearIndicator?: boolean; + }; + versionField?: { + id?: string; + emptyText?: string; + autocomplete?: boolean; + required?: boolean; + }; + showErrorIfItemNotInList?: boolean; + disabled?: boolean; +} + +export const ConductorNameVersionField = forwardRef< + { refetch: () => void }, + ConductorNameVersionFieldProps +>( + ( + { + label, + optionsUrl, + value, + nameField, + versionField, + onChange, + mapOptions, + showErrorIfItemNotInList = false, + disabled, + }, + ref, + ) => { + const { data, refetch } = useFetch(optionsUrl); + + // Expose the refetch method to the parent component via the ref + useImperativeHandle(ref, () => ({ + refetch, + })); + + const options: { + name: string; + versions: number[]; + }[] = useMemo(() => { + return mapOptions ? mapOptions(data) : data || []; + }, [data, mapOptions]); + + const versionOptions = useMemo(() => { + if (_isNil(value?.name)) { + return []; + } + const selectedOption = options.find(({ name }) => name === value?.name); + if (!selectedOption) { + return []; + } + return selectedOption.versions; + }, [options, value?.name]); + + return ( + <> + + + name)} + onChange={(_, val) => { + const selectedOption = options.find(({ name }) => name === val); + if (!selectedOption) { + onChange?.(undefined); + return; + } + onChange?.({ + name: val, + version: + versionField?.autocomplete || versionField?.required + ? _last(selectedOption.versions) + : undefined, + }); + }} + error={ + showErrorIfItemNotInList && + value != null && + !options.some(({ name }) => name === value?.name) + } + value={value?.name || null} + slotProps={ + nameField?.clearIndicator + ? { + clearIndicator: { + onClick: () => { + onChange?.(undefined); + }, + }, + } + : undefined + } + /> + + + + onChange?.({ + name: value?.name, + version: val === "Latest Version" ? undefined : Number(val), + }) + } + > + {!versionField?.required && value?.name && ( + + {versionField?.emptyText ?? "Latest version"} + + )} + {versionOptions.map((version) => ( + + {`Version ${version}`} + + ))} + + + + + ); + }, +); diff --git a/ui-next/src/components/v1/ConductorSelect.tsx b/ui-next/src/components/v1/ConductorSelect.tsx new file mode 100644 index 0000000000..5f2e6cb8b0 --- /dev/null +++ b/ui-next/src/components/v1/ConductorSelect.tsx @@ -0,0 +1,182 @@ +import { MenuItem, Button, Menu } from "@mui/material"; +import { ReactNode, useState, MouseEvent } from "react"; +import ConductorInput, { ConductorInputProps } from "./ConductorInput"; +import TagIcon from "@mui/icons-material/Tag"; +import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; + +import { styled } from "@mui/material/styles"; + +export type SelectItemType = + | { label: string; value: string | number } + | string + | number; + +export type ConductorSelectProps = ConductorInputProps & { + items?: SelectItemType[]; + children?: ReactNode; +}; + +const renderItems = (items: SelectItemType[]) => + Object.values(items).map((item) => { + if (typeof item === "string" || typeof item === "number") { + return ( + + {item} + + ); + } + + return ( + + {item.label} + + ); + }); + +const ConductorSelect = ({ + items, + children, + ...props +}: ConductorSelectProps) => { + return ( + + {items ? renderItems(items) : children} + + ); +}; + +const StyledButton = styled(Button)({ + textTransform: "none", + padding: "4px 8px", + backgroundColor: "#f0f0f0", + color: "#1976d2", + minHeight: "28px", + border: "none", + "&:hover": { + backgroundColor: "#e0e0e0", + border: "none", + }, + "& .MuiButton-startIcon, & .MuiButton-endIcon": { + color: "#666", + }, + "&.MuiButton-outlined": { + border: "none", + }, +}); + +const IconCircleWrapper = styled("div")(({ theme }) => ({ + display: "flex", + alignItems: "center", + justifyContent: "center", + border: `1px solid ${theme.palette.primary.main}`, + borderRadius: "50%", + width: "16px", + height: "16px", + padding: "4px", +})); + +export type HeadBarSelectProps = { + items?: SelectItemType[]; + children?: ReactNode; + value?: string | number; + onChange?: (value: string) => void; + label?: string; + fullWidth?: boolean; + labelOnEmpty?: string; +}; + +const HeadBarSelect = ({ + items, + children, + value, + onChange, + label, + fullWidth = true, + labelOnEmpty = "Select", +}: HeadBarSelectProps) => { + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + const hasOptions = (items && items.length > 0) || children; + + const handleClick = (event: MouseEvent) => { + if (hasOptions) { + setAnchorEl(event.currentTarget); + } + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const handleMenuItemClick = (newValue: string) => { + onChange?.(newValue); + handleClose(); + }; + + const displayValue = + label && value ? `${label} ${value}` : labelOnEmpty || value || "Select"; + + return ( + <> + + + + } + endIcon={hasOptions ? : null} + aria-controls={open ? "head-bar-menu" : undefined} + aria-haspopup={hasOptions ? "true" : undefined} + aria-expanded={open ? "true" : undefined} + variant="outlined" + > + {displayValue} + + + {items + ? items.map((item) => { + const itemValue = typeof item === "object" ? item.value : item; + const itemLabel = typeof item === "object" ? item.label : item; + + return ( + handleMenuItemClick(itemValue as string)} + selected={value === itemValue} + > + {itemLabel} + + ); + }) + : children} + + + ); +}; + +export { ConductorSelect, HeadBarSelect }; +export default ConductorSelect; diff --git a/ui-next/src/components/v1/ConductorSlider/ConductorSlider.tsx b/ui-next/src/components/v1/ConductorSlider/ConductorSlider.tsx new file mode 100644 index 0000000000..0eaedeb9f9 --- /dev/null +++ b/ui-next/src/components/v1/ConductorSlider/ConductorSlider.tsx @@ -0,0 +1,57 @@ +import { SliderProps } from "@mui/material"; +import { ChangeEvent, ReactNode } from "react"; +import ConductorSliderStateless from "./ConductorSliderStateless"; + +type ConductorSliderProps = SliderProps & { + label?: string | ReactNode; + textBox?: boolean; + onChangeValue: (value: number) => void; + sliderColor?: string; +}; + +function ConductorSlider({ + label, + min, + max, + textBox, + value, + onChangeValue, + sliderColor, + ...rest +}: ConductorSliderProps) { + const minValue = min ? min : 0; + const maxValue = max ? max : 100; + const handleInputChange = (event: ChangeEvent) => { + onChangeValue( + event.target.value === "" ? minValue : Number(event.target.value), + ); + }; + + const handleBlur = () => { + if (Number(value) < minValue) { + onChangeValue(minValue); + } else if (Number(value) > maxValue) { + onChangeValue(maxValue); + } + }; + const handleChange = (_e: Event, value: number | number[]) => { + onChangeValue(Array.isArray(value) ? value[0] : value); + }; + return ( + + ); +} + +export default ConductorSlider; +export type { ConductorSliderProps }; diff --git a/ui-next/src/components/v1/ConductorSlider/ConductorSliderStateless.tsx b/ui-next/src/components/v1/ConductorSlider/ConductorSliderStateless.tsx new file mode 100644 index 0000000000..791ada03f5 --- /dev/null +++ b/ui-next/src/components/v1/ConductorSlider/ConductorSliderStateless.tsx @@ -0,0 +1,107 @@ +import { Box, Slider, SliderProps, TextField, styled } from "@mui/material"; +import { ChangeEvent, ReactNode } from "react"; +import { blueLightMode, greyText } from "theme/tokens/colors"; + +const DEFAULT_SLIDER_COLOR = blueLightMode; + +const CustomSlider = styled(Slider, { + shouldForwardProp: (prop) => prop !== "sliderColor", +})<{ sliderColor?: string }>(({ sliderColor = DEFAULT_SLIDER_COLOR }) => ({ + height: "1px", + "& .MuiSlider-track": { + backgroundColor: sliderColor, + border: "0px", + }, + "& .MuiSlider-rail": { + backgroundColor: "#DDD", + }, + "& .MuiSlider-thumb": { + backgroundColor: sliderColor, + width: "17px", + height: "17px", + }, +})); + +type ConductorSliderStatelessProps = SliderProps & { + label?: string | ReactNode; + handleInputChange: (event: ChangeEvent) => void; + handleBlur: () => void; + textBox?: boolean; + sliderColor?: string; +}; + +const labelStyle = { + color: greyText, + fontSize: "12px", + fontWeight: 300, +}; + +const ConductorSliderStateless = ({ + label, + value, + min, + max, + handleBlur, + handleInputChange, + textBox, + sliderColor = DEFAULT_SLIDER_COLOR, + ...rest +}: ConductorSliderStatelessProps) => { + const textFieldStyle = { + marginLeft: "10px", + "& .MuiOutlinedInput-root": { + "&.Mui-focused fieldset": { + borderColor: sliderColor, + borderWidth: "1px", + }, + }, + }; + + return ( + + + {label && {label}} + {textBox && ( + + )} + + + + + + + ); +}; + +export type { ConductorSliderStatelessProps }; +export default ConductorSliderStateless; diff --git a/ui-next/src/components/v1/ConductorSplitButton.tsx b/ui-next/src/components/v1/ConductorSplitButton.tsx new file mode 100644 index 0000000000..de992b1bae --- /dev/null +++ b/ui-next/src/components/v1/ConductorSplitButton.tsx @@ -0,0 +1,192 @@ +import { + Fragment, + ReactNode, + useMemo, + useRef, + useState, + MouseEvent as ReactMouseEvent, + ReactElement, +} from "react"; +import ClickAwayListener from "@mui/material/ClickAwayListener"; +import Grow from "@mui/material/Grow"; +import Paper from "@mui/material/Paper"; +import Popper from "@mui/material/Popper"; +import MenuItem from "@mui/material/MenuItem"; +import MenuList from "@mui/material/MenuList"; +import MuiButton, { MuiButtonProps } from "components/MuiButton"; +import MuiButtonGroup, { MuiButtonGroupProps } from "components/MuiButtonGroup"; +import DropdownIcon from "./icons/DropdownIcon"; +import { colors } from "theme/tokens/variables"; +import { blueLight } from "theme/tokens/colors"; +import { Tooltip } from "@mui/material"; + +type ConductorSplitButtonProps = MuiButtonGroupProps & + MuiButtonProps & { + options: { + label: ReactNode; + onClick: () => void; + id?: string; + disabled?: boolean; + }[]; + primaryOnClick: () => void; + children: ReactNode; + tooltip?: string; + "data-testid"?: string; + }; + +const groupStyle = { + ".MuiButtonGroup-grouped": { + minWidth: "27px", + ":not(:last-of-type)": { + borderRight: "1px solid rgba(255, 255, 255, 0.5)", + }, + "&:hover:not(:last-of-type)": { + borderRight: "1px solid rgba(255, 255, 255, 0.5)", + }, + }, +}; + +export default function SplitButton({ + options, + primaryOnClick, + children, + startIcon, + tooltip, + id, + "data-testid": dataTestId, + ...props +}: ConductorSplitButtonProps) { + const [open, setOpen] = useState(false); + const anchorRef = useRef(null); + const handleClick = () => { + primaryOnClick(); + }; + + const handleMenuItemClick = ( + _event: ReactMouseEvent, + onClick: () => void, + ) => { + onClick(); + setOpen(false); + }; + + const handleToggle = () => { + setOpen((prevOpen) => !prevOpen); + }; + + const handleClose = (event: Event) => { + if ( + anchorRef.current && + anchorRef.current.contains(event.target as HTMLElement) + ) { + return; + } + + setOpen(false); + }; + + const Container = useMemo( + () => + ({ children }: { children: ReactElement }) => + props?.disabled ? ( + {children} + ) : ( + + {children} + + ), + [tooltip, props?.disabled], + ); + + return ( + + + + + {children} + + + + + + + + {({ TransitionProps, placement }) => ( + + + + + {options.map((option, index) => ( + + handleMenuItemClick(event, option.onClick) + } + > + {option.label} + + ))} + + + + + )} + + + ); +} + +export type { ConductorSplitButtonProps }; diff --git a/ui-next/src/components/v1/ConductorStringArrayFormField.tsx b/ui-next/src/components/v1/ConductorStringArrayFormField.tsx new file mode 100644 index 0000000000..d414b2bf94 --- /dev/null +++ b/ui-next/src/components/v1/ConductorStringArrayFormField.tsx @@ -0,0 +1,109 @@ +import { Grid, IconButton } from "@mui/material"; +import { Button } from "components"; +import { ConductorGroupFieldTitle } from "components/v1/ConductorGroupFieldTitle"; +import { ChangeEvent, FunctionComponent, ReactNode, useState } from "react"; +import { adjust, remove } from "utils"; +import { ConductorEmptyGroupField } from "./ConductorEmptyGroupField"; +import ConductorInput from "./ConductorInput"; +import AddIcon from "./icons/AddIcon"; +import TrashIcon from "./icons/TrashIcon"; + +interface ConductorStringArrayFormFieldProps { + inputParameters: string[]; + onChange: (newInputParams: string[]) => void; + someKey?: string; + addButtonLabel?: ReactNode; + label?: ReactNode; + title?: ReactNode; + compact?: boolean; + emptyListMessage?: ReactNode; +} + +export const ConductorStringArrayFormField: FunctionComponent< + ConductorStringArrayFormFieldProps +> = ({ + inputParameters = [], + onChange, + someKey = "", + addButtonLabel = "Add", + label = "Value", + title, + compact, + emptyListMessage, +}) => { + const [newItemValue, setNewItemValue] = useState(""); + const replaceItem = (newValue: string, index: number) => { + onChange(adjust(index, () => newValue, inputParameters)); + }; + + const deleteItem = (idx: number) => { + onChange(remove(idx, 1, inputParameters)); + }; + const addItem = () => { + onChange(inputParameters.concat(newItemValue)); + setNewItemValue(""); + }; + + const handleFocus = ( + e: ChangeEvent, + ) => { + e.target.select(); + }; + + return ( + <> + {title ? : null} + {inputParameters.length > 0 ? ( + <> + + {inputParameters.map((value, index) => ( + + + { + replaceItem(newValue, index); + }} + value={value} + onFocus={( + e: ChangeEvent, + ) => handleFocus(e)} + placeholder="e.g.: Cache-Control..." + sx={{ minWidth: "150px" }} + /> + + + + deleteItem(index)}> + + + + + ))} + + + + ) : ( + + )} + + ); +}; diff --git a/ui-next/src/components/v1/ConductorTabs.tsx b/ui-next/src/components/v1/ConductorTabs.tsx new file mode 100644 index 0000000000..a97225de20 --- /dev/null +++ b/ui-next/src/components/v1/ConductorTabs.tsx @@ -0,0 +1,61 @@ +import { Box, Tabs, TabsProps } from "@mui/material"; +import { blueLightMode, greyText2 } from "theme/tokens/colors"; + +const tabsStyle = { + height: "40px", + minHeight: "40px", + ".MuiButtonBase-root": { + padding: "0px", + }, + ".MuiTab-root": { + fontSize: "12px", + fontWeight: 500, + color: greyText2, + minWidth: "80px", + }, + "& .MuiTabs-indicator": { + display: "flex", + justifyContent: "center", + backgroundColor: "transparent", + }, + "& .MuiTabs-indicatorSpan": { + maxWidth: 40, + width: "100%", + backgroundColor: blueLightMode, + }, + + "& .MuiTab-root.Mui-selected": { + color: blueLightMode, + }, + ".MuiTabs-scrollButtons.Mui-disabled": { + opacity: 0.3, + }, +}; + +type ConductorTabsProps = TabsProps; + +const ConductorTabs = ({ + value, + onChange, + children, + ...props +}: ConductorTabsProps) => { + return ( + + , + }} + > + {children} + + + ); +}; + +export type { ConductorTabsProps }; +export default ConductorTabs; diff --git a/ui-next/src/components/v1/ConductorUpdateTaskFromEvent.tsx b/ui-next/src/components/v1/ConductorUpdateTaskFromEvent.tsx new file mode 100644 index 0000000000..af44671551 --- /dev/null +++ b/ui-next/src/components/v1/ConductorUpdateTaskFromEvent.tsx @@ -0,0 +1,116 @@ +import { FormControlLabel, Grid, Radio, RadioGroup } from "@mui/material"; +import ConductorInput, { + ConductorInputProps, +} from "components/v1/ConductorInput"; +import _omit from "lodash/omit"; +import { ComponentType, useMemo } from "react"; + +type EventTaskReferenceInput = { taskId: string }; +type WorkflowTaskReferenceInput = { workflowId: string; taskRefName: string }; +export type EventJson = Partial< + EventTaskReferenceInput & WorkflowTaskReferenceInput +>; + +interface FormWithRadioGroupProps { + value: EventJson; + onChange: (value: EventJson) => void; + inputComponent?: ComponentType; +} + +const omitTaskId = (value: EventJson) => _omit(value, "taskId"); + +const omitWorkflowID = (value: EventJson) => + _omit(value, ["workflowId", "taskRefName"]); + +const _isTaskIdSelected = (value: EventJson) => value?.taskId != null; + +export const ConductorUpdateTaskFormEvent = ({ + value, + onChange, + inputComponent: InputComponent = ConductorInput, +}: FormWithRadioGroupProps) => { + const isTaskIdSelected = useMemo(() => _isTaskIdSelected(value), [value]); + + return ( + <> + label >span": { fontWeight: 600, mb: 2 } }} + name="refresh-radio-group-options" + row + value={isTaskIdSelected ? "task-id" : "workflow-id-task-ref"} + onChange={(event) => { + if (event.target.value === "task-id") { + onChange({ + taskId: value.taskId ?? "", + }); + } else { + onChange({ + workflowId: value.workflowId ?? "", + taskRefName: value.taskRefName ?? "", + }); + } + }} + > + } + label="Workflow Id + Task Ref Name" + value="workflow-id-task-ref" + id="workflow-and-task-ref-radio-button" + /> + } + label="Task Id" + value="task-id" + id="task-id-radio-button" + /> + + {isTaskIdSelected ? ( + + + onChange(omitWorkflowID({ ...value, taskId: val })) + } + /> + + ) : ( + + + + onChange(omitTaskId({ ...value, workflowId: val })) + } + /> + + + + onChange(omitTaskId({ ...value, taskRefName: val })) + } + /> + + + )} + + ); +}; diff --git a/ui-next/src/components/v1/CopyClipboardButton.tsx b/ui-next/src/components/v1/CopyClipboardButton.tsx new file mode 100644 index 0000000000..328e852e36 --- /dev/null +++ b/ui-next/src/components/v1/CopyClipboardButton.tsx @@ -0,0 +1,45 @@ +import IconButton, { IconButtonProps } from "@mui/material/IconButton"; +import { useContext } from "react"; + +import CopyIcon from "components/v1/icons/CopyIcon"; +import { MessageContext } from "components/v1/layout/MessageContext"; + +export type CopyClipboardButtonProps = IconButtonProps & { + text: string; + message?: string; +}; + +export const CopyClipboardButton = ({ + text, + message = "Copied to clipboard!", + onClick, +}: CopyClipboardButtonProps) => { + const { setMessage } = useContext(MessageContext); + + return ( + { + navigator.clipboard.writeText(text); + setMessage({ + text: message, + severity: "success", + }); + + if (onClick) { + onClick(event); + } + }} + sx={{ + color: (theme) => theme.palette.primary.main, + "&:hover": { + backgroundColor: "transparent", + }, + padding: 0, + marginLeft: 1, + }} + > + + + ); +}; diff --git a/ui-next/src/components/v1/EventExpressionHelp.tsx b/ui-next/src/components/v1/EventExpressionHelp.tsx new file mode 100644 index 0000000000..a9cd1e4bda --- /dev/null +++ b/ui-next/src/components/v1/EventExpressionHelp.tsx @@ -0,0 +1,228 @@ +import { Box } from "@mui/material"; +import MuiTypography from "components/MuiTypography"; +import { useState } from "react"; +import { ConductorAutoComplete } from "./ConductorAutoComplete"; + +const EVENT_COLORS = [ + "#4FAAD1", + "#6569AC", + "#45AC59", + "#C99E00", + "#EE6B31", + "#CE2836", +]; + +const checkForWarningMessages = ( + labels: string[], + suggestions: string[], + value: string, +) => { + if (!value) { + return ""; + } + const valueParts = value.split(":"); + + if (valueParts.length > labels.length) { + return "Warning: The event should not contain more than three parts separated by colons"; + } + + // Check if first term exists in the suggestions + const firstTermExists = suggestions.some( + (suggestion) => suggestion.split(":")[0] === valueParts[0], + ); + + // Check if second term exists in the suggestions + const secondTermExists = suggestions.some( + (suggestion) => suggestion.split(":")[1] === valueParts[1], + ); + + if (valueParts[0] && !firstTermExists) + return `Warning: ${valueParts[0]} is not a valid ${labels[0]}`; + if (valueParts[1] && !secondTermExists) + return `Warning: ${valueParts[1]} is not a valid ${labels[1]}`; +}; + +const getItems = (labels: string[], value: string) => { + const valueParts = value?.split(":"); + + const items = labels?.map((label, index) => ({ + label, + value: valueParts?.[index] || "", + })); + + return items; +}; + +export interface EventExpressionHelpProps { + labels: string[]; + suggestions: string[]; + width: number; + height: number; + value: string; + onChange: (value: string) => void; +} + +export const EventExpressionHelp = ({ + labels, + suggestions, + width, + height, + value = "", + onChange = () => {}, +}: EventExpressionHelpProps) => { + const [data, setData] = useState({ + warning: checkForWarningMessages(labels, suggestions, value), + items: getItems(labels, value), + }); + const [highlightedPart, setHighlightedPart] = useState(null); + + const handleEventChange = (val: string) => { + onChange(val); + const updatedItems = getItems(labels, val); + const updatedWarning = checkForWarningMessages(labels, suggestions, val); + setData((prevState) => ({ + ...prevState, + items: updatedItems, + warning: updatedWarning, + })); + }; + + const getHighlightedPart = (value: string, selectionStart: number) => { + const partsUntilCursor = value.substring(0, selectionStart).split(":"); + setHighlightedPart(partsUntilCursor.length - 1); + }; + + return ( + + + + EVENT EXPRESSIONS HELP + + + + + {data?.items.map((_, index) => { + const xStep = width / data?.items.length; + const yStep = height / data?.items.length; + const colorIndex = index % EVENT_COLORS.length; + return ( + + ); + })} + + + {data?.items.map((_, index) => { + const blockWidth = width / data?.items.length; + const colorIndex = index % EVENT_COLORS.length; + return ( + + * + + ); + })} + + + + + {data?.items.map((item, index) => { + const blockHeight = height / data?.items.length; + const colorIndex = index % EVENT_COLORS.length; + return ( + + {item.label}: {item.value} + + ); + })} + + + + + handleEventChange(val)} + onInputChange={(_, val) => handleEventChange(val)} + freeSolo + selectOnFocus + onBlur={(_e) => setHighlightedPart(null)} + onKeyDown={(e: any) => + getHighlightedPart(e.target.value, e.target.selectionStart) + } + onKeyUp={(e: any) => + getHighlightedPart(e.target.value, e.target.selectionStart) + } + helperText={data?.warning} + error={data?.warning ? true : false} + /> + + ); +}; diff --git a/ui-next/src/components/v1/FileUploadButton.tsx b/ui-next/src/components/v1/FileUploadButton.tsx new file mode 100644 index 0000000000..00a1123187 --- /dev/null +++ b/ui-next/src/components/v1/FileUploadButton.tsx @@ -0,0 +1,143 @@ +import AttachIcon from "@mui/icons-material/AttachFile"; +import { Box, useTheme } from "@mui/material"; +import IconButton from "@mui/material/IconButton"; +import Stack from "@mui/material/Stack"; +import Button, { MuiButtonProps } from "components/MuiButton"; +import { ChangeEvent, ElementType } from "react"; +import { colors } from "theme/tokens/variables"; +import XCloseIcon from "./icons/XCloseIcon"; + +export interface FileUploadButtonProps extends MuiButtonProps { + value?: string; + onChangeFile: (fileName: string, fileValue: string) => void; + onClearFile?: () => void; + accept?: string; + component?: ElementType; + label?: string; + helperText?: string; + error?: boolean; +} + +const ACCEPTED_TYPES = + ".json,application/json, .jks,application/octet-stream, .pem,application/x-pem-file, .creds,application/octet-stream"; + +export default function FileUploadButton({ + value, + onChangeFile: handleChange, + onClearFile, + accept = ACCEPTED_TYPES, + label, + helperText, + error, + ...props +}: FileUploadButtonProps) { + const theme = useTheme(); + + const stylesForError = { + border: "1px solid red", + color: theme.palette.input.error, + "&:hover": { + border: "1px solid red", + color: theme.palette.input.error, + boxShadow: `3px 3px 0px 0px ${theme.palette.input.error}`, + }, + "&:active": { + color: colors.black, + boxShadow: "0px", + backgroundColor: theme.palette.input.error, + }, + "&:focus": { + boxShadow: "0px", + }, + }; + + const stylesWithoutTheError = { + "&:active": { + color: colors.black, + boxShadow: "0px", + }, + "&:focus": { + boxShadow: "0px", + }, + }; + + const handleFileChange = (event: ChangeEvent) => { + const files = event.target.files; + if (files) { + const firstFile = files[0]; + const reader = new FileReader(); + reader.readAsDataURL(firstFile); + reader.onload = () => { + handleChange(firstFile?.name, reader.result as string); + }; + } + }; + + return ( + <> + {value ? ( + + + {value} + {onClearFile && ( + + + + )} + + ) : ( + + + No file chosen + + )} + + {helperText} + + + ); +} diff --git a/ui-next/src/components/v1/FlatMapForm/ConductorAutocompleteArrayField.tsx b/ui-next/src/components/v1/FlatMapForm/ConductorAutocompleteArrayField.tsx new file mode 100644 index 0000000000..3324cc897e --- /dev/null +++ b/ui-next/src/components/v1/FlatMapForm/ConductorAutocompleteArrayField.tsx @@ -0,0 +1,143 @@ +import { Grid } from "@mui/material"; +import Button from "components/MuiButton"; +import IconButton from "components/MuiIconButton"; +import AddIcon from "components/v1/icons/AddIcon"; +import TrashIcon from "components/v1/icons/TrashIcon"; +import _isEmpty from "lodash/isEmpty"; +import maybeVariable from "pages/definition/EditorPanel/TaskFormTab/forms/maybeVariableHOC"; +import { FunctionComponent, ReactNode } from "react"; +import { adjust, remove } from "utils"; +import { ButtonPosition } from "utils/constants/common"; +import { ConductorAutocompleteVariables } from "./ConductorAutocompleteVariables"; + +interface RemovableFieldProps { + onChange: (value: any) => void; + value?: any; + onRemove?: () => void; + addButtonPosition?: ButtonPosition; + isError?: boolean; + hasAtLeastOne?: boolean; + placeholder?: string; + label?: ReactNode; +} + +const getWidth = (currentWidth: number, addButtonPosition?: ButtonPosition) => { + if (addButtonPosition === ButtonPosition.RIGHT) { + return currentWidth - 2; + } + + return currentWidth; +}; + +const RemovableField: FunctionComponent = ({ + onChange, + value, + onRemove, + addButtonPosition, + isError, + hasAtLeastOne, + placeholder, + label, +}) => { + const isEmptyValue = _isEmpty(value?.trim()); + + return ( + <> + + onChange(val)} + error={isError && isEmptyValue} + /> + + {!hasAtLeastOne && ( + + {onRemove && ( + onRemove!()}> + + + )} + + )} + + ); +}; + +interface ConductorAutocompleteArrayFieldProps { + value: any[]; + onChange: (val: any[]) => void; + addButtonPosition?: ButtonPosition; + isError?: boolean; + hasAtLeastOne?: boolean; + placeholder?: string; + label?: ReactNode; +} + +const ConductorAutocompleteArrayFieldBase: FunctionComponent< + ConductorAutocompleteArrayFieldProps +> = ({ + value = [], + onChange, + addButtonPosition, + isError, + hasAtLeastOne, + placeholder, + label = "Value", +}) => { + const handleValueChange = (index: number) => (newValue: string) => { + onChange(adjust(index, () => newValue, value)); + }; + const handleRemoveValue = (index: number) => () => { + onChange(remove(index, 1, value)); + }; + const handleAddItem = () => onChange(value.concat("")); + + return ( + + {value.map((val, index) => ( + + ))} + + + + + ); +}; + +const AutocompleteArrayField = maybeVariable( + ConductorAutocompleteArrayFieldBase, +); +export { AutocompleteArrayField }; diff --git a/ui-next/src/components/v1/FlatMapForm/ConductorAutocompleteVariables.tsx b/ui-next/src/components/v1/FlatMapForm/ConductorAutocompleteVariables.tsx new file mode 100644 index 0000000000..484f6b8914 --- /dev/null +++ b/ui-next/src/components/v1/FlatMapForm/ConductorAutocompleteVariables.tsx @@ -0,0 +1,490 @@ +import { + Autocomplete, + AutocompleteRenderOptionState, + InputLabelProps, + Popper, +} from "@mui/material"; +import { SxProps } from "@mui/system"; +import { useSelector } from "@xstate/react"; +import match from "autosuggest-highlight/match"; +import parse from "autosuggest-highlight/parse"; +import ConductorInput, { + ConductorInputProps, +} from "components/v1/ConductorInput"; +import XCloseIcon from "components/v1/icons/XCloseIcon"; +import _initial from "lodash/initial"; +import _isNil from "lodash/isNil"; +import { TaskFormContext } from "pages/definition/EditorPanel/TaskFormTab/state"; +import { WorkflowMetadataContext } from "pages/definition/WorkflowMetadata/state"; +import { + DefinitionMachineContext, + WorkflowEditContext, +} from "pages/definition/state"; +import { useGetVariablesForSelectedTasks } from "pages/definition/state/useGetVariablesForSelectedTasks"; +import { + CSSProperties, + FunctionComponent, + HTMLAttributes, + KeyboardEvent, + ReactNode, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { autocompleteStyle } from "shared/styles"; +import { CoerceToType, TaskDef } from "types/common"; +import { DEFAULT_WF_ATTRIBUTES } from "utils/constants"; +import { checkCoerceTypeError } from "utils/helpers"; +import { ActorRef, State } from "xstate"; +import { customFilterOptions, VARIABLE_REGEX } from "./formOptions"; + +type CohercesToNumber = "integer" | "double"; + +type TypeCohersionNumber = { + onChange: (change: number) => void; + coerceTo: CohercesToNumber; +}; +type TypeCohersionString = { + onChange: (change: string) => void; + coerceTo?: "string"; +}; +type TypeCohersion = TypeCohersionNumber | TypeCohersionString; + +export type ConductorAutocompleteVariablesProps = { + label?: string | ReactNode; + value?: string | number; + fullWidth?: boolean; + placeholder?: string; + helperText?: string; + taskBranches?: TaskDef[]; + workflowTasks?: TaskDef[]; + workflowInputParameters?: string[]; + actor?: ActorRef; + otherOptions?: string[] | number[]; + openOnFocus?: boolean; + secrets?: string[]; + envs?: string[]; + InputLabelProps?: InputLabelProps; + sxInput?: SxProps; + onFocus?: () => void; + growPopper?: boolean; + workflowActor?: ActorRef; + onKeyDown?: (value: KeyboardEvent) => void; + error?: boolean; + id?: string; + inputProps?: ConductorInputProps; + required?: boolean; + multiline?: boolean; + variables?: string[]; + disabled?: boolean; + onInputChange?: (val: any) => void; + onBlur?: (val: string) => void; + renderOption?: ( + props: HTMLAttributes, + option: string | number, + state: AutocompleteRenderOptionState, + ) => ReactNode; + getOptionLabel?: (option: string | number) => string; +} & TypeCohersion; + +const assertOnChangeNumber = ( + onChange: any, + coerceTo?: CoerceToType, +): onChange is (n: number) => void => { + return coerceTo != null && ["integer", "double"].includes(coerceTo!); +}; + +const assertOnChangeString = ( + onChange: any, + coerceTo?: CoerceToType, +): onChange is (n: string) => void => { + return coerceTo == null || ["string"].includes(coerceTo!); +}; + +const replaceLastrDolarWithValue = (currentValue: string, newValue: string) => { + return currentValue.slice(0, -1) + newValue; +}; + +const ConductorAutocompleteVariablesNoContext = ({ + onChange, + label, + value = "", + fullWidth = true, + required = false, + placeholder, + helperText, + taskBranches = [], + workflowTasks = [], + workflowInputParameters = [], + otherOptions = [], + openOnFocus = false, + secrets = [], + envs = [], + InputLabelProps, + sxInput, + coerceTo = "string", + onFocus, + growPopper, + onKeyDown, + error = false, + id, + inputProps, + multiline = false, + variables = [], + disabled, + onInputChange, + onBlur, + renderOption, + getOptionLabel: customGetOptionLabel, +}: ConductorAutocompleteVariablesProps) => { + const inputRef = useRef(null); + const [expandField, setExpandField] = useState(false); + + useEffect(() => { + if (expandField && inputRef.current) { + inputRef.current.focus(); + inputRef.current.selectionStart = inputRef.current.value.length; + inputRef.current.selectionEnd = inputRef.current.value.length; + } + }, [expandField]); + + const options = useMemo(() => { + const taskOptions = _initial(taskBranches!).reduce( + (result, task: TaskDef) => { + if (task) { + const { taskReferenceName } = task; + + return [...result, `\${${taskReferenceName}.output}`]; + } + + return result; + }, + [] as string[], + ); + + const workflowTaskOptions = workflowTasks?.map( + (task: TaskDef) => `\${${task.taskReferenceName}.output}`, + ); + + const taskJoinOn = _initial(taskBranches!).reduce( + (result, task: TaskDef) => { + if ( + task && + task.type === "JOIN" && + task.joinOn && + task.joinOn.length > 0 + ) { + const { joinOn } = task; + const joinOnSpread = joinOn.map((item) => `\${${item}.output}`); + return [...result, ...joinOnSpread]; + } + + return result; + }, + [] as string[], + ); + + const workflowInputOptions = workflowInputParameters?.map( + (ip: string) => "${workflow.input." + ip + "}", + ); + + const secretNamesOptions = secrets?.map( + (name: string) => "${workflow.secrets." + name + "}", + ); + + const envNameOptions = envs?.map((n) => "${workflow.env." + n + "}"); + + const variableOptions = variables?.map( + (name: string) => "${workflow.variables." + name + "}", + ); + + const workflowAttributes = DEFAULT_WF_ATTRIBUTES?.map( + (item) => `\${${item}}`, + ); + return (otherOptions as string[]) + .concat(`\${workflow.input}`) + .concat(`\${workflow.secrets}`) + .concat(`\${workflow.env}`) + .concat( + workflowTaskOptions, + taskOptions, + taskJoinOn, + workflowInputOptions, + workflowAttributes, + secretNamesOptions, + envNameOptions, + variableOptions, + ); + }, [ + taskBranches, + workflowTasks, + workflowInputParameters, + secrets, + otherOptions, + envs, + variables, + ]); + + const isErrorValue = useMemo( + () => checkCoerceTypeError({ value, coerceTo }), + [value, coerceTo], + ); + + const popperStyle = (style: CSSProperties | undefined) => { + return growPopper ? {} : style; + }; + + return ( + ( + + )} + filterOptions={(options, { inputValue }) => + customFilterOptions(options, inputValue) + } + renderInput={(params) => ( + + )} + value={value?.toString() ?? ""} + freeSolo + disabled={disabled} + onFocus={() => { + onFocus?.(); + if (!multiline) { + setExpandField(true); + } + }} + onBlur={() => { + if (!multiline) { + setExpandField(false); + } + if (onBlur && inputRef?.current && inputRef?.current?.value) { + onBlur(inputRef?.current?.value); + return; + } + }} + openOnFocus={openOnFocus} + autoComplete + onChange={(a, b) => { + if (!_isNil(b) && b !== value) { + if (assertOnChangeNumber(onChange, coerceTo) && !isNaN(b as any)) { + onChange(Number(b)); + } else if (assertOnChangeString(onChange, coerceTo)) { + if (typeof b === "string") { + const newValue = b as string; + const currentValue = value.toString(); + if (currentValue.endsWith("$")) { + onChange(replaceLastrDolarWithValue(currentValue, newValue)); + } else if (VARIABLE_REGEX.test(currentValue)) { + onChange(currentValue.replace(VARIABLE_REGEX, newValue)); + } else { + onChange(newValue); + } + } else { + onChange(b); + } + } + } + }} + onInputChange={(event: any, o) => { + if (onInputChange) { + onInputChange(o); + return; + } + if (o !== value) { + if (assertOnChangeNumber(onChange, coerceTo) && !isNaN(o as any)) { + onChange(Number(o)); + } else if (assertOnChangeString(onChange, coerceTo)) { + onChange(String(o)); + } else { + (onChange as (val: unknown) => void)(o); + } + } + }} + getOptionLabel={ + customGetOptionLabel + ? customGetOptionLabel + : (option: string | number) => { + if (typeof option === "number") { + return option.toString(); + } + return option; + } + } + renderOption={ + renderOption + ? renderOption + : (props, option, { inputValue }) => { + const matches = match(option as string, inputValue); + const parts = parse(option as string, matches); + + return ( +
  • +
    + {parts.map((part, index) => ( + + {part.text} + + ))} +
    +
  • + ); + } + } + options={options} + sx={[ + autocompleteStyle({ value }), + // Add padding for clear icon when multiline to prevent text overlap + (expandField || multiline) && value + ? { + "& .MuiTextField-root .MuiOutlinedInput-root.MuiInputBase-root": { + paddingRight: "48px", + }, + "& .MuiTextField-root .MuiInputBase-multiline.MuiOutlinedInput-root": + { + paddingRight: "48px", + }, + "& .MuiTextField-root textarea.MuiInputBase-input.MuiOutlinedInput-input": + { + paddingRight: "0px", + }, + } + : null, + ]} + clearIcon={} + /> + ); +}; + +const AutocompleteVariablesWithFormContext: FunctionComponent< + ConductorAutocompleteVariablesProps +> = ({ actor: formTaskActor, workflowActor, ...restProps }) => { + const taskBranches = useSelector( + formTaskActor!, + (current) => current.context.tasksBranch, + ); + + const workflowInputParameters = useSelector( + formTaskActor!, + (current) => current.context.workflowInputParameters, + ); + // fetching secrets from context + const secrets = useSelector(workflowActor!, (state) => state.context.secrets); + + const workflowVariableInputs = useGetVariablesForSelectedTasks(workflowActor); + + const envsObj = + useSelector( + workflowActor!, + (state: State) => state.context?.envs, + ) || {}; + // refining secrets with just secretNames + const secretNames = + secrets && secrets.length > 0 ? secrets.map((item: any) => item?.name) : []; + return ( + + ); +}; + +const AutocompleteVariablesWithWorkflowMetadataContext: FunctionComponent< + ConductorAutocompleteVariablesProps +> = ({ actor: metadataActor, workflowActor, ...restProps }) => { + const taskBranches: TaskDef[] = []; + const workflowTasks = useSelector( + metadataActor!, + (current) => current.context.metadataChanges.tasks, + ); + const workflowInputParameters = useSelector( + metadataActor!, + (current) => current.context.metadataChanges.inputParameters, + ); + // fetching secrets from context + const secrets = useSelector(workflowActor!, (state) => state.context.secrets); + + const workflowVariableInputs = useGetVariablesForSelectedTasks(workflowActor); + + const envsObj = + useSelector( + workflowActor!, + (state: State) => state.context?.envs, + ) || {}; + + // refining secrets with just secretNames + const secretNames = + secrets && secrets.length > 0 ? secrets.map((item: any) => item?.name) : []; + + const envsOptions = Object.keys(envsObj); + + return ( + + ); +}; + +export const ConductorAutocompleteVariables: FunctionComponent< + ConductorAutocompleteVariablesProps +> = (props) => { + const { formTaskActor } = useContext(TaskFormContext); + const { workflowMetadataActor } = useContext(WorkflowMetadataContext); + const { workflowDefinitionActor } = useContext(WorkflowEditContext); + + if (!_isNil(formTaskActor) && !_isNil(workflowDefinitionActor)) { + return ( + + ); + } else if ( + !_isNil(WorkflowMetadataContext) && + !_isNil(workflowDefinitionActor) + ) { + return ( + + ); + } + return ; +}; diff --git a/ui-next/src/components/v1/FlatMapForm/ConductorFieldTypeDropdown.tsx b/ui-next/src/components/v1/FlatMapForm/ConductorFieldTypeDropdown.tsx new file mode 100644 index 0000000000..1bb0b1b612 --- /dev/null +++ b/ui-next/src/components/v1/FlatMapForm/ConductorFieldTypeDropdown.tsx @@ -0,0 +1,84 @@ +import { FunctionComponent } from "react"; +import { + FIELD_TYPE_BOOLEAN, + FIELD_TYPE_NULL, + FIELD_TYPE_NUMBER, + FIELD_TYPE_OBJECT, + FIELD_TYPE_STRING, + FieldType, +} from "types/common"; +import ConductorSelect from "components/v1/ConductorSelect"; +import { ConductorTooltipProps } from "components/conductorTooltip/ConductorTooltip"; + +const fieldTypeOption: FieldType[] = [ + FIELD_TYPE_STRING, + FIELD_TYPE_NUMBER, + FIELD_TYPE_BOOLEAN, + FIELD_TYPE_NULL, + FIELD_TYPE_OBJECT, +]; + +function getFieldTypeLabel(type: FieldType): string { + switch (type) { + case FIELD_TYPE_NUMBER: + return "Number"; + case FIELD_TYPE_BOOLEAN: + return "Boolean"; + case FIELD_TYPE_NULL: + return "Null"; + case FIELD_TYPE_OBJECT: + return "Object/Array"; + default: + return "String"; + } +} +interface ConductorFieldTypeDropdownProps { + label?: string; + type: FieldType; + onTypeChange: (value: FieldType) => void; + hideObjectArray?: boolean; + allowedTypes?: FieldType[]; + tooltip?: Omit; +} + +const ConductorFieldTypeDropdown: FunctionComponent< + ConductorFieldTypeDropdownProps +> = ({ + label, + type, + onTypeChange, + hideObjectArray, + allowedTypes = fieldTypeOption, + tooltip, +}) => { + const filteredOptions = fieldTypeOption.reduce< + { + label: string; + value: string; + }[] + >((result, type) => { + if ( + allowedTypes.includes(type) && + (!hideObjectArray || type !== FIELD_TYPE_OBJECT) + ) { + return [...result, { label: getFieldTypeLabel(type), value: type }]; + } + + return result; + }, []); + + return ( + { + onTypeChange(ev.target.value as FieldType); + }} + tooltip={tooltip} + items={filteredOptions} + /> + ); +}; + +export default ConductorFieldTypeDropdown; diff --git a/ui-next/src/components/v1/FlatMapForm/ConductorFlatMapForm.tsx b/ui-next/src/components/v1/FlatMapForm/ConductorFlatMapForm.tsx new file mode 100644 index 0000000000..11677087fa --- /dev/null +++ b/ui-next/src/components/v1/FlatMapForm/ConductorFlatMapForm.tsx @@ -0,0 +1,197 @@ +import { Box, Grid } from "@mui/material"; +import Button from "components/MuiButton"; +import { ConductorEmptyGroupField } from "components/v1/ConductorEmptyGroupField"; +import { ConductorGroupFieldTitle } from "components/v1/ConductorGroupFieldTitle"; +import { ConductorKeyValueInput } from "components/v1/FlatMapForm/ConductorKeyValueInput"; +import AddIcon from "components/v1/icons/AddIcon"; +import _omit from "lodash/omit"; +import maybeVariable from "pages/definition/EditorPanel/TaskFormTab/forms/maybeVariableHOC"; +import { FunctionComponent, ReactNode, useMemo } from "react"; +import { SWITCH_CASE_PREFIX } from "utils/constants/switch"; +import { getSequentiallySuffix, randomChars } from "utils/strings"; +import { ConductorAutocompleteVariables } from "./ConductorAutocompleteVariables"; + +export interface ConductorFlatMapFormProps { + title?: string | null; + addItemLabel?: string; + keyColumnLabel?: string; + valueColumnLabel?: string; + typeColumnLabel?: string; + hideValue?: boolean; + hideButtons?: boolean; + showFieldTypes?: boolean; + value?: Record; + onChange?: (newValues: Record) => void; + hiddenKeys?: string[]; + someKey?: string; + enableAutocomplete?: boolean; + autoFocusField?: boolean; + customInput?: ReactNode; + keyGenFunction?: () => string; + valGenFunction?: () => string; + focusOnField?: string; + isSwitchCase?: boolean; + placeholder?: string; + compact?: boolean; + emptyListMessage?: ReactNode; + otherOptions?: string[]; +} + +const ConductorFlatMapFormBase: FunctionComponent< + ConductorFlatMapFormProps +> = ({ + title = null, + addItemLabel = "Add", + keyColumnLabel = "Key", + valueColumnLabel = "Value", + typeColumnLabel = "Type", + hideButtons = false, + hideValue = false, + showFieldTypes = false, + value = {}, + onChange = (_newValues) => {}, + hiddenKeys, + someKey = "", + autoFocusField = true, + customInput, + keyGenFunction = () => `SomeKey${randomChars()}`, + valGenFunction = () => `Some-val-${randomChars()}`, + focusOnField, + isSwitchCase, + enableAutocomplete = true, + placeholder, + compact, + emptyListMessage, + otherOptions = [], +}: ConductorFlatMapFormProps) => { + const [valueEntries, entryKeys] = useMemo(() => { + const entries = Object.entries(value); + const entryKeys = Object.keys(value); + return [entries, entryKeys]; + }, [value]); + + const noVisibleKeys = valueEntries.every(([key]) => + hiddenKeys?.includes(key), + ); + + const replaceItem = ([newKey, newValue]: [string, any], idx: number) => { + const modifiedPreservingOrder = Object.fromEntries( + valueEntries.map(([key, val], innerIdx) => + innerIdx === idx ? [newKey, newValue] : [key, val], + ), + ); + + onChange(modifiedPreservingOrder); + }; + + const addParameter = () => { + const sequentialSuffix = (name: string) => + getSequentiallySuffix({ + name: name, + refNames: entryKeys, + }).name; + const tempKey = isSwitchCase + ? sequentialSuffix(SWITCH_CASE_PREFIX) + : keyGenFunction(); + const tempValue = isSwitchCase ? ([] as any) : valGenFunction(); + + const newValues = { + ...value, + [tempKey]: tempValue, + }; + + onChange(newValues); + }; + + const deleteItem = (key: string) => { + onChange(_omit(value, key)); + }; + + return ( + <> + {title ? : null} + + {noVisibleKeys ? ( + + ) : ( + <> + + {valueEntries && ( + + {valueEntries.reduce((acc: Array, [key, val], index) => { + return !hiddenKeys?.includes(key) + ? acc.concat( + + { + replaceItem([newKey, val], index); + }} + onChangeValue={(newValue: any) => { + replaceItem([key, newValue], index); + }} + placeholder={placeholder} + onDeleteItem={deleteItem} + hideValue={hideValue} + showFieldTypes={showFieldTypes} + hideButtons={hideButtons} + autoFocusField={autoFocusField} + keyColumnLabel={keyColumnLabel} + valueColumnLabel={valueColumnLabel} + typeColumnLabel={typeColumnLabel} + customInput={ + enableAutocomplete ? ( + { + replaceItem([key, newValue], index); + }} + /> + ) : ( + customInput + ) + } + focusOnField={focusOnField} + /> + , + ) + : acc; + }, [])} + + )} + + {!hideButtons ? ( + + ) : null} + + )} + + ); +}; + +const ConductorFlatMapForm = maybeVariable(ConductorFlatMapFormBase); + +export { ConductorFlatMapForm, ConductorFlatMapFormBase }; diff --git a/ui-next/src/components/v1/FlatMapForm/ConductorKeyValueInput.tsx b/ui-next/src/components/v1/FlatMapForm/ConductorKeyValueInput.tsx new file mode 100644 index 0000000000..3919dfc9a7 --- /dev/null +++ b/ui-next/src/components/v1/FlatMapForm/ConductorKeyValueInput.tsx @@ -0,0 +1,283 @@ +import { Grid } from "@mui/material"; +import MuiCheckbox from "components/MuiCheckbox"; +import IconButton from "components/MuiIconButton"; +import { ConductorCodeBlockInput } from "components/v1/ConductorCodeBlockInput"; +import ConductorInput from "components/v1/ConductorInput"; +import ConductorInputNumber from "components/v1/ConductorInputNumber"; +import TrashIcon from "components/v1/icons/TrashIcon"; +import { ConductorTooltipProps } from "components/conductorTooltip/ConductorTooltip"; +import _isEmpty from "lodash/isEmpty"; +import { + ChangeEvent, + FunctionComponent, + ReactNode, + useEffect, + useRef, + useState, +} from "react"; +import { + FIELD_TYPE_BOOLEAN, + FIELD_TYPE_NULL, + FIELD_TYPE_NUMBER, + FIELD_TYPE_OBJECT, + FieldType, +} from "types/index"; +import { castToType, inferType } from "utils"; +import { ConductorStringOrJsonInput } from "./ConductorStringOrJsonInput"; + +interface ConductorKeyValueInputProps { + onChangeValue: (a: string | number | boolean | null) => void; + mKey: string; + onChangeKey: (k: string) => void; + onDeleteItem: (k: string) => void; + value: string | Record; + hideValue: boolean; + existingKeys: string[]; + hideButtons: boolean; + showFieldTypes: boolean; + focusOnField?: string; + keyColumnLabel?: string; + valueColumnLabel?: string; + typeColumnLabel?: string; + enableAutocomplete?: boolean; + autoFocusField?: boolean; + tooltip?: { + type?: Omit; + key?: Omit; + value?: Omit; + }; + customInput?: ReactNode; + placeholder?: string; +} + +export type MaybeInputProps = { + cantCoerce: boolean; + isContainsError: boolean; + objValue: string; + onChangeValue: (value: any) => void; + onObjChange: (a: string) => void; + type?: FieldType; + value: any; + valueColumnLabel?: string; + customInput?: ReactNode; + tooltip?: Omit; + placeholder?: string; +}; + +export const MaybeInput = (props: MaybeInputProps) => { + const { + cantCoerce, + objValue, + onChangeValue, + onObjChange, + type, + value, + valueColumnLabel = "", + tooltip, + isContainsError, + customInput, + placeholder = "e.g.: max-age=...", + } = props; + + switch (type) { + case FIELD_TYPE_NULL: + return null; + + case FIELD_TYPE_BOOLEAN: + return ( + onChangeValue(event.target.checked)} + /> + ); + + case FIELD_TYPE_NUMBER: + return ( + { + if (value === null) return onChangeValue(""); + return onChangeValue(castToType(value, inferType(value))); + }} + value={value} + inputProps={{ + allowNegative: true, + thousandSeparator: false, + valueIsNumericString: true, + }} + label={valueColumnLabel} + tooltip={tooltip} + /> + ); + + case FIELD_TYPE_OBJECT: + return ( + + ); + + default: + return customInput ? ( + customInput + ) : ( + + onChangeValue(castToType(val, inferType(value))) + } + value={value} + placeholder={placeholder} + helperText={isContainsError} + label={valueColumnLabel} + showClearButton + /> + ); + } +}; + +export const ConductorKeyValueInput: FunctionComponent< + ConductorKeyValueInputProps +> = ({ + onChangeValue, + onChangeKey, + mKey, + hideValue, + value, + existingKeys, + hideButtons, + showFieldTypes, + onDeleteItem, + focusOnField, + keyColumnLabel, + valueColumnLabel, + autoFocusField, + tooltip, + customInput, + placeholder, +}) => { + const inputRef = useRef(null); + const [valueAnEr, setValueAnEr] = useState<[string, string]>([mKey, ""]); + const [currentType, _setCurrentType] = useState(inferType(value)); + + const handleUpdateValue = (val: string) => { + if (existingKeys.includes(val)) { + setValueAnEr([val, "Key should be unique"]); + } else { + setValueAnEr([val, ""]); + onChangeKey(val); + } + }; + + const firstValue = valueAnEr[0]; + + useEffect(() => { + if (focusOnField && inputRef !== null && firstValue === focusOnField) { + const inputChildRef = inputRef?.current?.querySelector("input"); + inputChildRef?.focus(); + } + }, [inputRef, focusOnField, firstValue]); + + const containsError = !_isEmpty(valueAnEr[1]); + + const handleFocus = ( + e: ChangeEvent, + ) => { + e.target.select(); + }; + + const dynamicWidth = () => { + if (hideValue) { + return 12; + } + + return 4; + }; + + return ( + + + + + , + ) => handleFocus(e)} + fullWidth + placeholder="e.g.: Cache-Control..." + error={containsError} + helperText={valueAnEr[1]} + ref={inputRef} + label={keyColumnLabel} + showClearButton + tooltip={tooltip?.key} + /> + + + {!hideValue && ( + + + + )} + + + {!hideButtons ? ( + + onDeleteItem(mKey)}> + + + + ) : null} + + ); +}; diff --git a/ui-next/src/components/v1/FlatMapForm/ConductorStringOrJsonInput.tsx b/ui-next/src/components/v1/FlatMapForm/ConductorStringOrJsonInput.tsx new file mode 100644 index 0000000000..e68f6c62b9 --- /dev/null +++ b/ui-next/src/components/v1/FlatMapForm/ConductorStringOrJsonInput.tsx @@ -0,0 +1,256 @@ +import { Box, IconButton, Tooltip } from "@mui/material"; +import { + ReactNode, + useState, + useMemo, + cloneElement, + isValidElement, +} from "react"; +import { ConductorCodeBlockInput } from "components/v1/ConductorCodeBlockInput"; +import { useCoerceToObject } from "utils/utils"; +import type { ConductorAutocompleteVariablesProps } from "./ConductorAutocompleteVariables"; +import { TextTIcon } from "@phosphor-icons/react"; +import ConductorInput from "../ConductorInput"; + +export type ConductorStringOrJsonInputProps = Omit< + ConductorAutocompleteVariablesProps, + "onChange" | "value" +> & { + value: string | Record; + onChange: (value: string | number | boolean | null) => void; + helperText?: string; + error?: boolean; + customInput?: ReactNode; + showFieldTypes: boolean; + placeholder?: string; +}; + +const JsonIcon = ({ size = 14 }: { size?: number }) => ( + + {"{}"} + +); + +type ToggleButtonProps = { + isJsonMode: boolean; + onToggle: () => void; +}; + +const ToggleButton = ({ isJsonMode, onToggle }: ToggleButtonProps) => ( + + + + {isJsonMode ? ( + + ) : ( + + )} + + + +); + +export const ConductorStringOrJsonInput = ({ + value, + onChange, + label, + helperText, + error = false, + customInput, + showFieldTypes, + placeholder = "e.g.: max-age=...", +}: ConductorStringOrJsonInputProps) => { + // Determine if value is JSON/object + const isValueJson = useMemo(() => { + if (value == null || value === "") return false; + if (typeof value === "object") return true; + return false; + }, [value]); + + const [isJsonMode, setIsJsonMode] = useState(isValueJson); + + const stringifyJson = (value: string | Record) => { + return JSON.stringify(value, null, 2); + }; + + // Get string value for text input + const stringValue = useMemo(() => { + if (typeof value === "object") { + return stringifyJson(value); + } + const strValue = value == null ? "" : String(value); + + // If the value is a JSON stringified string (starts and ends with quotes), parse it + if (strValue.startsWith('"') && strValue.endsWith('"')) { + try { + const parsed = JSON.parse(strValue); + if (typeof parsed === "string") { + // Display with quotes to indicate it's a string + return `"${parsed}"`; + } + } catch { + // Not valid JSON, return as-is + } + } + + // For numbers and booleans, display without quotes + // For other strings, display as-is + return strValue; + }, [value]); + + const [onObjChange, objValue, cantCoerce] = useCoerceToObject( + onChange, + value, + ); + + const handleStringChange = (newValue: string | number) => { + const strValue = String(newValue).trim(); + + // Empty string + if (strValue === "") { + onChange(""); + return; + } + + // If the value is quoted (starts and ends with quotes), treat as string + if ( + (strValue.startsWith('"') && strValue.endsWith('"')) || + (strValue.startsWith("'") && strValue.endsWith("'")) + ) { + // Remove quotes and pass as JSON stringified string + const unquoted = strValue.slice(1, -1); + onChange(unquoted); + return; + } + + // Try to parse as boolean (case-insensitive) + const lowerValue = strValue.toLowerCase(); + if (lowerValue === "true" || lowerValue === "false") { + onChange(lowerValue === "true"); + return; + } + if (strValue === "null") { + onChange(null); + return; + } + + // Try to parse as number + // Use a regex to check if it's a valid number format (integers, decimals, scientific notation) + const numberRegex = /^-?\d+(\.\d+)?([eE][+-]?\d+)?$/; + if (numberRegex.test(strValue)) { + const numValue = Number(strValue); + if (!isNaN(numValue) && isFinite(numValue)) { + onChange(numValue); // Pass as number string (valid JSON) + return; + } + } + + if (strValue === "{}" || strValue === "[]") { + onChange(JSON.parse(strValue)); + setIsJsonMode(true); + return; + } + onChange(strValue); + }; + + const handleToggleMode = () => { + onChange(""); + setIsJsonMode(!isJsonMode); + }; + + if (isJsonMode) { + const jsonValue = stringifyJson(value); + const isEmptyJsonValue = jsonValue === "{}" || jsonValue === "[]"; + return ( + + + {showFieldTypes && ( + + )} + + ); + } + + const renderCustomInput = () => { + if (!customInput) return null; + + if (isValidElement(customInput)) { + const existingProps = customInput.props || {}; + return cloneElement(customInput, { + ...existingProps, + onChange: handleStringChange, + onTextInputChange: handleStringChange, + value: stringValue, + } as Record); + } + + return customInput; + }; + + return ( + + {customInput ? ( + renderCustomInput() + ) : ( + + )} + + {showFieldTypes && ( + + )} + + ); +}; diff --git a/ui-next/src/components/v1/FlatMapForm/customFilter.test.ts b/ui-next/src/components/v1/FlatMapForm/customFilter.test.ts new file mode 100644 index 0000000000..60a64edbb8 --- /dev/null +++ b/ui-next/src/components/v1/FlatMapForm/customFilter.test.ts @@ -0,0 +1,47 @@ +import { customFilterOptions } from "./formOptions"; + +const options = [ + "GET", + "POST", + "${workflow.input}", + "${workflow.secrets}", + "${workflow.env}", + "${workflow.output}", + "${workflow.env.name}", + "${workflow.env.testing}", + "${workflow.env.golfClub}", + "${workflow.env.company}", +]; +const optionsWithDollar = [ + "${workflow.input}", + "${workflow.secrets}", + "${workflow.env}", + "${workflow.output}", + "${workflow.env.name}", + "${workflow.env.testing}", + "${workflow.env.golfClub}", + "${workflow.env.company}", +]; +const optionsWithInputText = [ + "${workflow.env}", + "${workflow.env.name}", + "${workflow.env.testing}", + "${workflow.env.golfClub}", + "${workflow.env.company}", +]; +const inputvalueWithDollar = "GET$"; +const inputvalueWithIncompleteVariable = "GET${workflow.env"; + +describe("customFilterOptions", () => { + it("return all options start with $", () => { + const result = customFilterOptions(options as any, inputvalueWithDollar); + expect(result).toEqual(optionsWithDollar); + }); + it("return all options start with ${workflow.env", () => { + const result = customFilterOptions( + options as any, + inputvalueWithIncompleteVariable, + ); + expect(result).toEqual(optionsWithInputText); + }); +}); diff --git a/ui-next/src/components/v1/FlatMapForm/formOptions.ts b/ui-next/src/components/v1/FlatMapForm/formOptions.ts new file mode 100644 index 0000000000..f6a2676db4 --- /dev/null +++ b/ui-next/src/components/v1/FlatMapForm/formOptions.ts @@ -0,0 +1,25 @@ +import _first from "lodash/first"; + +export const VARIABLE_REGEX = /\$\{[^}]*$/; + +export const customFilterOptions = (options: string[], inputValue: string) => { + const sanitizedInputValue = inputValue.toString().toLowerCase(); + if (sanitizedInputValue.endsWith("$")) { + return options?.filter((option) => + option?.toString().toLowerCase().startsWith("$"), + ); + } else if (VARIABLE_REGEX.test(sanitizedInputValue)) { + const matchedValue = _first(sanitizedInputValue.match(VARIABLE_REGEX)); + if (matchedValue) { + return options?.filter((option) => + option?.toString().toLowerCase().startsWith(matchedValue), + ); + } else { + return []; + } + } else { + return options?.filter((option) => + option?.toString().toLowerCase().startsWith(sanitizedInputValue), + ); + } +}; diff --git a/ui-next/src/components/v1/HeadTabs.tsx b/ui-next/src/components/v1/HeadTabs.tsx new file mode 100644 index 0000000000..38d38a60f4 --- /dev/null +++ b/ui-next/src/components/v1/HeadTabs.tsx @@ -0,0 +1,65 @@ +import { Tabs, TabsProps } from "@mui/material"; +import { + tabsColor, + tabActiveColor, + tabBackground, + white, +} from "theme/tokens/colors"; + +const tabsStyle = (pl = "100px") => { + return { + ".MuiTabs-scroller": { + backgroundColor: "transparent", + }, + ".MuiTabs-flexContainer": { + background: "transparent", + paddingLeft: pl, + }, + ".MuiButtonBase-root": { + color: tabsColor, + fontSize: "16px", + fontWeight: 600, + background: tabBackground, + borderRadius: "10px 10px 0px 0px", + margin: "0 2px", + }, + ".Mui-selected": { + color: tabActiveColor, + background: white, + }, + ".MuiTabs-indicator": { + display: "none", + }, + "&.MuiTabs-root": { + width: "fit-content", + }, + "@media(max-width:1025px)": { + ".MuiTabs-flexContainer": { + paddingRight: "0px", + justifyContent: "left", + paddingLeft: "0px", + }, + }, + }; +}; + +interface HeadTabsProps extends TabsProps { + pl?: string; +} + +const HeadTabs = ({ + value, + onChange, + children, + pl, + ...props +}: HeadTabsProps) => { + return ( + + {children} + + ); +}; + +export type { HeadTabsProps }; +export default HeadTabs; diff --git a/ui-next/src/components/v1/Modal/ConfirmModal.tsx b/ui-next/src/components/v1/Modal/ConfirmModal.tsx new file mode 100644 index 0000000000..d71f255e70 --- /dev/null +++ b/ui-next/src/components/v1/Modal/ConfirmModal.tsx @@ -0,0 +1,96 @@ +import { Close } from "@mui/icons-material"; +import { + Dialog, + DialogActions, + DialogContent, + DialogTitle, +} from "@mui/material"; +import ActionButton from "components/ActionButton"; +import Button from "components/MuiButton"; +import MuiTypography from "components/MuiTypography"; +import SaveIcon from "components/v1/icons/SaveIcon"; +import XCloseIcon from "components/v1/icons/XCloseIcon"; +import { ReactNode } from "react"; +import { modalStyles } from "./commonStyles"; + +type ConfirmModalProps = { + title: string; + titleIcon: ReactNode; + progressLoading: boolean; + handleSave: (data: any) => void; + handleClose: () => void; + content: ReactNode; + disableSaveBtn?: boolean; + disableBackdropClick?: boolean; + disableCancelBtn?: boolean; + id?: string; +}; + +const ConfirmModal = ({ + title, + titleIcon, + progressLoading, + handleSave, + content, + handleClose, + disableSaveBtn, + disableBackdropClick, + disableCancelBtn, + id = "confirm-modal-wrapper", +}: ConfirmModalProps) => { + const onClose = ( + event: Event, + reason: "backdropClick" | "escapeKeyDown" | "closeButtonClick", + ) => { + if (reason === "backdropClick" && disableBackdropClick) { + return false; + } + + handleClose(); + }; + + return ( + + + {titleIcon ? {titleIcon} : null} + + {title} + + + + {content} + + + } + onClick={handleSave} + disabled={disableSaveBtn} + > + Save + + + + ); +}; + +export type { ConfirmModalProps }; +export default ConfirmModal; diff --git a/ui-next/src/components/v1/Modal/UnsavedChangesDialog.tsx b/ui-next/src/components/v1/Modal/UnsavedChangesDialog.tsx new file mode 100644 index 0000000000..cb0681c22d --- /dev/null +++ b/ui-next/src/components/v1/Modal/UnsavedChangesDialog.tsx @@ -0,0 +1,106 @@ +import CloseIcon from "@mui/icons-material/Close"; +import WarningRoundedIcon from "@mui/icons-material/WarningRounded"; +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogProps, + DialogTitle, + IconButton, + Typography, +} from "@mui/material"; +import { ReactNode } from "react"; + +interface UnsavedChangesDialogProps extends DialogProps { + handleAction: () => void; + handleCancel: () => void; + handleDiscard: () => void; + header?: ReactNode; + message?: ReactNode; + actionButtonLabel?: string; + hasErrors?: boolean; +} + +const UnsavedChangesDialog = ({ + handleAction, + handleCancel, + handleDiscard, + actionButtonLabel = "Save", + header = "Unsaved Changes", + message = "You have unsaved changes. What would you like to do?", + hasErrors = false, + ...dialogProps +}: UnsavedChangesDialogProps) => { + return ( + + + {hasErrors && } + {header} + theme.palette.grey[500], + }} + > + + + + + + {message} + + + + + + + + + + + + + ); +}; + +export default UnsavedChangesDialog; diff --git a/ui-next/src/components/v1/Modal/commonStyles.ts b/ui-next/src/components/v1/Modal/commonStyles.ts new file mode 100644 index 0000000000..95d1bd3b7a --- /dev/null +++ b/ui-next/src/components/v1/Modal/commonStyles.ts @@ -0,0 +1,37 @@ +export const modalStyles = { + dialog: { + ".MuiPaper-root": { + width: " 420px", + "& input:-webkit-autofill, & input:-webkit-autofill:hover, & input:-webkit-autofill:focus, & input:-webkit-autofill:active": + { + WebkitBoxShadow: "0 0 0px 1000px white inset", + }, + ".MuiAlert-root": { + width: "100%", + }, + }, + }, + title: { + background: "transparent", + borderBottom: "none", + display: "flex", + gap: 2, + position: "relative", + left: "-10px", + }, + closeIcon: { + position: "absolute", + right: 10, + cursor: "pointer", + border: "2px solid", + borderRadius: "50%", + color: "rgba(175, 175, 175, 1)", + }, + groupDescInput: { + minHeight: "96px", + marginTop: "-10px", + "& textArea": { padding: 0, border: "none" }, + "& .MuiOutlinedInput-notchedOutline": { border: "none" }, + "& p": { margin: "0px 12px 4px" }, + }, +}; diff --git a/ui-next/src/components/v1/Modal/useDialogHelper.tsx b/ui-next/src/components/v1/Modal/useDialogHelper.tsx new file mode 100644 index 0000000000..6e056b8ccb --- /dev/null +++ b/ui-next/src/components/v1/Modal/useDialogHelper.tsx @@ -0,0 +1,81 @@ +import AdminPanelSettingsOutlinedIcon from "@mui/icons-material/AdminPanelSettingsOutlined"; +import CancelOutlinedIcon from "@mui/icons-material/CancelOutlined"; +import ManageAccountsOutlinedIcon from "@mui/icons-material/ManageAccountsOutlined"; +import PersonOutlineOutlinedIcon from "@mui/icons-material/PersonOutlineOutlined"; +import VisibilityIcon from "@mui/icons-material/Visibility"; +import { AutocompleteRenderGetTagProps, Chip } from "@mui/material"; +import { SvgIconProps } from "@mui/material/SvgIcon"; +import { roleLabel } from "utils/roles"; +import { ComponentType } from "react"; +import { greyText } from "theme/tokens/colors"; + +interface RoleConfig { + [key: string]: { + icon: ComponentType; + color: string; + chipBackground: string; + }; +} + +export const roleConfig: RoleConfig = { + ADMIN: { + icon: AdminPanelSettingsOutlinedIcon, + color: "rgba(64, 186, 86, 1)", + chipBackground: "rgba(159, 220, 170, 1)", + }, + USER: { + icon: PersonOutlineOutlinedIcon, + color: "#9157FF", + chipBackground: "rgba(200, 171, 255, 1)", + }, + WORKFLOW_MANAGER: { + icon: ManageAccountsOutlinedIcon, + color: "rgba(27, 194, 244, 1)", + chipBackground: "rgba(141, 224, 249, 1)", + }, + METADATA_MANAGER: { + icon: ManageAccountsOutlinedIcon, + color: "rgba(251, 164, 4, 1)", + chipBackground: "rgba(252, 209, 129, 1)", + }, + USER_READ_ONLY: { + icon: VisibilityIcon, + color: greyText, + chipBackground: "rgba(221, 221, 221, 1)", + }, +}; + +export const getOptionIcon = (roleName: string) => { + const role = roleConfig[roleName]; + if (!role) { + return null; + } + const { icon: IconComponent, color } = role; + return ; +}; + +export const renderRoleTags = ( + value: string[], + getTagProps: AutocompleteRenderGetTagProps, +) => + value.map((option, index) => { + const { chipBackground } = roleConfig[option] || {}; + const { key, ...otherTagProps } = getTagProps({ index }); + return ( + } + sx={{ + backgroundColor: chipBackground, + color: "#000", + borderRadius: "30px", + "& .MuiSvgIcon-root": { + background: "transparent", + fill: "black", + }, + }} + /> + ); + }); diff --git a/ui-next/src/components/v1/MultiOptionSelect.tsx b/ui-next/src/components/v1/MultiOptionSelect.tsx new file mode 100644 index 0000000000..e5bc5d7541 --- /dev/null +++ b/ui-next/src/components/v1/MultiOptionSelect.tsx @@ -0,0 +1,87 @@ +import { Chip } from "@mui/material"; +import Autocomplete, { + AutocompleteProps, + AutocompleteRenderInputParams, +} from "@mui/material/Autocomplete"; +import TextField, { TextFieldProps } from "@mui/material/TextField"; +import { SyntheticEvent } from "react"; + +interface Option { + value: string; + label: string; + color: string; +} + +interface MultiOptionSelectProps extends Omit< + AutocompleteProps, + "renderInput" +> { + options: Option[]; + label: string; + autoFocus?: boolean; + error?: boolean; + required?: boolean; + helperText?: string; + value: Option[]; + onChange: (_event: SyntheticEvent, value: Option[]) => void; + inputProps?: TextFieldProps["inputProps"]; +} + +const MultiOptionSelect = ({ + options, + label, + autoFocus, + error, + required, + helperText, + value, + onChange, + inputProps, + ...props +}: MultiOptionSelectProps) => { + return ( + option.label} + value={value} + onChange={onChange} + filterSelectedOptions + renderTags={(value: readonly Option[], getTagProps) => + value.map((option: Option, index: number) => { + const { key, ...otherTagProps } = getTagProps({ index }); + return ( + + ); + }) + } + renderInput={(params: AutocompleteRenderInputParams) => ( + + )} + {...props} + /> + ); +}; + +export type { MultiOptionSelectProps }; +export default MultiOptionSelect; diff --git a/ui-next/src/components/v1/RoundedInput.tsx b/ui-next/src/components/v1/RoundedInput.tsx new file mode 100644 index 0000000000..c6cb27e377 --- /dev/null +++ b/ui-next/src/components/v1/RoundedInput.tsx @@ -0,0 +1,144 @@ +import { + Box, + InputAdornment, + TextField, + TextFieldProps, + IconButton, + Theme, + SxProps, +} from "@mui/material"; +import { forwardRef, useState, ChangeEvent, useRef, Ref } from "react"; +import { blueLightMode } from "theme/tokens/colors"; +import XCloseIcon from "./icons/XCloseIcon"; + +const style = { + inputStyle: { + "& .MuiOutlinedInput-notchedOutline, & .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline": + { + border: "none", + }, + "& .MuiTextField-root, & .MuiInputBase-root ": { + fontSize: "12px", + minHeight: "30px", + borderRadius: "20px", + padding: "0px", + px: "4px", + background: (theme: Theme) => theme.palette.input.background, + }, + "& .MuiInputAdornment-positionStart": { + width: "12px", + }, + "& .MuiInputAdornment-positionEnd": { + paddingRight: "4px", + }, + }, +}; + +export type RoundedInputProps = Omit & { + placeholder?: string; + autoFocus?: boolean; + required?: boolean; + multiline?: boolean; + onBlur?: (value: string) => void; + onChange?: (value: string) => void; + icon?: any; + clearButton?: boolean; + textFieldSx?: SxProps; +}; + +export const RoundedInput = forwardRef( + ( + { + placeholder, + autoFocus, + onBlur = () => null, + multiline, + onChange = () => null, + icon, + clearButton = true, + textFieldSx, + ...rest + }: RoundedInputProps, + ref: Ref, + ) => { + const [isFocused, setIsFocused] = useState(autoFocus); + const inputRef = useRef(null); + const handleFocus = () => { + setIsFocused(true); + }; + + const handleBlur = (event: any) => { + setIsFocused(false); + if (typeof onBlur === "function") { + const { value } = event.target; + onBlur(value); + } + }; + + const handleChange = ( + event: ChangeEvent, + ) => { + if (onChange) { + const { value } = event.target; + onChange(value); + } + }; + + const clearValue = () => { + if (onChange) { + onChange(""); + } + if (inputRef.current !== null) { + inputRef.current.focus(); + } + }; + return ( + + {icon} + ), + }), + ...(clearButton && { + endAdornment: ( + + + + + + ), + }), + }} + {...rest} + /> + + ); + }, +); diff --git a/ui-next/src/components/v1/SpinningIcon.tsx b/ui-next/src/components/v1/SpinningIcon.tsx new file mode 100644 index 0000000000..f64ed79261 --- /dev/null +++ b/ui-next/src/components/v1/SpinningIcon.tsx @@ -0,0 +1,19 @@ +import { keyframes, styled } from "@mui/system"; +import { IconProps } from "@phosphor-icons/react"; +import { ForwardRefExoticComponent, RefAttributes } from "react"; + +const spin = keyframes` + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +`; +const iconStyle = ({ loading }: { loading: boolean }) => ({ + animation: loading ? `${spin} 1s forwards` : "none", +}); + +export const SpinningIcon = ( + Icon: ForwardRefExoticComponent>, +) => styled(Icon)(iconStyle); diff --git a/ui-next/src/components/v1/SwaggerTestComponent.tsx b/ui-next/src/components/v1/SwaggerTestComponent.tsx new file mode 100644 index 0000000000..443a472a8e --- /dev/null +++ b/ui-next/src/components/v1/SwaggerTestComponent.tsx @@ -0,0 +1,383 @@ +import { + Box, + Grid, + Tab, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Tabs, +} from "@mui/material"; +import { Button, Input } from "components"; + +import React, { useState } from "react"; +import { CodeSnippet } from "./CodeSnippet"; +import { Method, ServiceDefDto } from "types/RemoteServiceTypes"; + +function getColorScheme(method: any) { + switch (method) { + case "POST": + return { + primaryColor: "#49cc90", + secondaryColor: "rgba(73,204,144,.1)", + }; + case "GET": + return { + primaryColor: "#61affe", + secondaryColor: "rgba(97,175,254,.1)", + }; + case "DELETE": + return { + primaryColor: "#f93e3e", + secondaryColor: "rgba(249,62,62,.1)", + }; + case "PUT": + return { + primaryColor: "#fca130", + secondaryColor: "rgba(252,161,48,.1)", + }; + default: + return { + primaryColor: "#000000", // Default color + secondaryColor: "rgba(0,0,0,.1)", + }; + } +} + +async function apiFetch( + methodType: string, + endpoint: string, + body?: any, + token?: string, +) { + const options = { + method: methodType, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token || ""}`, + }, + ...(body && { body: JSON.stringify(body) }), + }; + + const response = await fetch(endpoint, options); + + return response; +} +function replaceDynamicParams( + url: string, + params: Record, +): string { + return url.replace(/\{(\w+)\}/g, (_, key: string): string => { + return key in params ? String(params[key]) : `{${key}}`; + }); +} + +const SwaggerTestComponent = ({ + data, + serviceDefinition, +}: { + data: Method; + serviceDefinition: Partial; +}) => { + const [requestParams, setRequestParams] = useState>({}); + + const handleExecute = () => { + if (data?.methodType && serviceDefinition?.serviceURI) { + const updatedMethodName = replaceDynamicParams( + data?.methodName, + requestParams, + ); + apiFetch( + data?.methodType, + serviceDefinition?.serviceURI + updatedMethodName, + ); + } + }; + + const handleInputChange = (name: string, value: string) => { + const updatedRequestParams = { ...requestParams, [name]: value }; + setRequestParams(updatedRequestParams); + }; + return ( + + {/* header */} + + + + {data?.methodType} + + + + + {data?.methodName} + + + + {/* body */} + + {/* params */} + + {}} + aria-label="basic tabs" + TabIndicatorProps={{ + style: { + backgroundColor: + (data?.methodType && + getColorScheme(data?.methodType)?.primaryColor) ?? + "gray", + height: "4px", + }, + }} + sx={{ + "& .MuiTab-root": { + color: "#3b4151", + fontWeight: 800, + fontSize: "14px", + }, + }} + > + + + + + + + + + Name + Description + + + + {data?.requestParams && data?.requestParams?.length > 0 ? ( + data?.requestParams?.map((row) => ( + + + + + {row.name} + {row.required && ( + + * required + + )} + + + ({row.type}) + + + + + + handleInputChange(row.name, value) + } + sx={{ + maxWidth: "fit-content", + background: "#ffffff", + }} + /> + + + )) + ) : ( + No parameters + )} + +
    +
    + + + +
    + {/* response */} + + + {}} + aria-label="basic tabs" + TabIndicatorProps={{ + style: { + height: "0px", + }, + }} + sx={{ + "& .MuiTab-root": { + color: "#3b4151", + fontWeight: 800, + fontSize: "14px", + }, + }} + > + + + + + + Request URL + + + + + Server response + + + + + + Code + Details + + + + + + 403 + + + + + + +
    +
    +
    +
    +
    +
    +
    + ); +}; + +export default SwaggerTestComponent; diff --git a/ui-next/src/components/v1/TagList.tsx b/ui-next/src/components/v1/TagList.tsx new file mode 100644 index 0000000000..ad759b05b4 --- /dev/null +++ b/ui-next/src/components/v1/TagList.tsx @@ -0,0 +1,43 @@ +import { Box } from "@mui/material"; +import { TagDto } from "../../types/Tag"; +import TagChip from "../TagChip"; + +interface TagListProps { + tags?: TagDto[]; + name: string; + sx?: Record; + style?: Record; +} + +export const TagList = ({ + tags, + name, + sx = { mr: 2, mt: 1 }, + style, +}: TagListProps) => { + if (!tags?.length) return null; + + return ( + + {tags.map((tag) => { + if (!tag) return null; + const { key, value } = tag; + return ( + + ); + })} + + ); +}; + +export default TagList; + +export const TagsRenderer = ( + tags: TagDto[], + row: T, +) => ; diff --git a/ui-next/src/components/v1/TestTask/TestTask.tsx b/ui-next/src/components/v1/TestTask/TestTask.tsx new file mode 100644 index 0000000000..fa473f5523 --- /dev/null +++ b/ui-next/src/components/v1/TestTask/TestTask.tsx @@ -0,0 +1,473 @@ +import { RocketLaunch } from "@mui/icons-material"; +import { + Box, + FormControlLabel, + IconButton, + Link, + Radio, + RadioGroup, + Tooltip, + TooltipProps, + styled, +} from "@mui/material"; +import CircularProgress from "@mui/material/CircularProgress"; +import MuiButton from "components/MuiButton"; +import MuiTypography from "components/MuiTypography"; +import WorkflowStatusBadge from "components/WorkflowStatusBadge"; +import _assoc from "lodash/fp/assoc"; +import { ChangeEvent, FunctionComponent, useMemo, useState } from "react"; +import { + FormSectionProps, + JsonSectionProps, + TestControlsProps, + TestOutputProps, + TestTaskProps, +} from "types/TestTaskTypes"; +import { extractVariablesFromJSON } from "utils/json"; +import { tryToJson } from "utils/utils"; +import { ConductorCodeBlockInput } from "../ConductorCodeBlockInput"; +import ConductorInput from "../ConductorInput"; +import XCloseIcon from "../icons/XCloseIcon"; + +const CustomisedTooltip = styled(({ className, ...props }: TooltipProps) => ( + +))(() => ({ + "& .MuiTooltip-tooltip": { + backgroundColor: "white", + color: "rgba(6, 6, 6, 1)", + minWidth: 450, + width: "100%", + filter: "drop-shadow(0px 0px 6px rgba(89, 89, 89, 0.41))", + borderRadius: "6px", + padding: "15px 10px 10px 15px", + border: "1px solid #0D94DB", + }, + "& .MuiTooltip-arrow": { + color: "white", + fontSize: "28px", + "&:before": { + border: "1px solid #0D94DB", + }, + }, +})); + +const closeIconStyle = { + position: "absolute", + right: 0, + cursor: "pointer", +}; + +const FormSection: FunctionComponent = ({ + extractedJsonVariables, + value, + onChangeModel, + domain, + onChangeDomain, +}) => { + const handleVariableChange = (val: string, newValue: string) => { + const keysWithValue = Object.keys(extractedJsonVariables).filter( + (key) => extractedJsonVariables[key] === val, + ); + const updatedJsonValue = keysWithValue.reduce( + (acc, key) => _assoc(key, newValue, acc), + { ...value }, + ); + onChangeModel(updatedJsonValue); + }; + + const uniqueValues = Array.from( + new Set(Object.values(extractedJsonVariables)), + ) as string[]; + + return ( + + {uniqueValues.map((val) => { + const keyForTargetValue = + Object.keys(extractedJsonVariables).find( + (key) => extractedJsonVariables[key] === val, + ) || ""; + return ( + + + handleVariableChange(val, event.target.value) + } + /> + + ); + })} + + onChangeDomain(event.target.value)} + data-testid="test-task-domain-input" + /> + + + ); +}; + +const JsonSection: FunctionComponent = ({ + handleJSONChange, + taskModel, + value, + domain, + onChangeDomain, +}) => { + return ( + + handleJSONChange(val)} + value={JSON.stringify({ ...taskModel, ...value }, null, 2)} + containerStyles={{ borderColor: "#ffffff" }} + minHeight={150} + options={{ + lineNumbers: "off", + }} + /> + + onChangeDomain(event.target.value)} + data-testid="test-task-domain-input" + /> + + + ); +}; + +const TestControls: FunctionComponent = ({ + taskModel, + value, + isInProgress, + handleRunTestTask, + onChangeModel, + domain, + onChangeDomain, + showForm, +}) => { + const [toggleValue, setToggleValue] = useState(showForm ? "form" : "json"); + const extractedJsonVariables = extractVariablesFromJSON(taskModel); + + const handleToggleChange = (e: ChangeEvent) => { + setToggleValue(e.target.value); + }; + + const handleJSONChange = (newValue: string) => { + const parsedObject = tryToJson(newValue); + + if (parsedObject) { + onChangeModel(parsedObject as Record); + } + }; + + return ( + + + + {showForm && ( + } + label="Form" + /> + )} + } label="JSON" /> + + } + label="Provide the task inputs via:" + sx={{ + marginLeft: 0, + marginBottom: 2, + "& .MuiFormControlLabel-label": { + color: "#858585", + fontWeight: 500, + fontSize: 12, + }, + "& .MuiFormControlLabel-root .MuiFormControlLabel-label": { + fontWeight: 300, + }, + }} + /> + {toggleValue === "json" ? ( + + ) : ( + + )} + + + } + color="primary" + id="run-test-task" + disabled={isInProgress} + onClick={handleRunTestTask} + sx={{ + "& .MuiButton-startIcon": { + margin: 0, + }, + }} + > + Run Test + + + + ); +}; + +const InProgressState: FunctionComponent = () => { + return ( + + Running... + + + ); +}; + +const TestOutput: FunctionComponent = ({ + testedTaskExecutionResult, + onChangeModel, + status, + testExecutionId, +}) => { + const handleJSONChange = (newValue: string) => { + const parsedObject = tryToJson(newValue); + + if (parsedObject) { + onChangeModel(parsedObject as Record); + } + }; + + return ( + + + Output will appear below when test is complete. + + + handleJSONChange(val)} + value={JSON.stringify( + testedTaskExecutionResult?.tasks?.[0]?.outputData as Record< + string, + unknown + >, + null, + 2, + )} + containerStyles={{ borderColor: "#ffffff" }} + minHeight={150} + options={{ + lineNumbers: "off", + }} + /> + + + + + + + + Click the link to view the full execution: + + + + {`${testExecutionId}`} + + + + + + ); +}; + +export const TestTask = ({ + taskModel, + onChangeModel, + domain, + onChangeDomain, + value, + maxHeight, + handleRunTestTask, + isInProgress, + onDismiss, + testExecutionId, + testedTaskExecutionResult, + showForm, +}: TestTaskProps) => { + const status = testedTaskExecutionResult?.status; + + const renderContents = useMemo(() => { + if (status) { + return ( + + ); + } else if (isInProgress) { + return ; + } else { + return ( + + ); + } + }, [ + status, + isInProgress, + domain, + value, + taskModel, + testExecutionId, + testedTaskExecutionResult, + handleRunTestTask, + onChangeDomain, + onChangeModel, + showForm, + ]); + + return ( + + + + + Test Task + + + + + + + + {renderContents} +
    + } + > + } + color="secondary" + > + Test Task + + + ); +}; diff --git a/ui-next/src/components/v1/TestTask/index.ts b/ui-next/src/components/v1/TestTask/index.ts new file mode 100644 index 0000000000..8a6877570a --- /dev/null +++ b/ui-next/src/components/v1/TestTask/index.ts @@ -0,0 +1 @@ +export * from "./TestTask"; diff --git a/ui-next/src/components/v1/TooltipModal.tsx b/ui-next/src/components/v1/TooltipModal.tsx new file mode 100644 index 0000000000..f56f57c751 --- /dev/null +++ b/ui-next/src/components/v1/TooltipModal.tsx @@ -0,0 +1,89 @@ +import { + Box, + Theme, + Tooltip, + TooltipProps, + styled, + useMediaQuery, +} from "@mui/material"; +import MuiTypography from "components/MuiTypography"; +import { ReactNode } from "react"; +import { colors } from "theme/tokens/variables"; + +const CustomisedTooltip = styled( + ({ className, ...props }: TooltipProps & { isMobile: boolean }) => ( + + ), +)(({ isMobile }) => ({ + zIndex: 1099, // reduced from default (1500) to prevent appearing above AppBar, Drawer, Modal, and Snackbar + "& .MuiTooltip-tooltip": { + backgroundColor: "white", + color: "rgba(6, 6, 6, 1)", + minWidth: isMobile ? 300 : 600, + width: "100%", + filter: "drop-shadow(0px 0px 6px rgba(89, 89, 89, 0.41))", + borderRadius: "6px", + border: `2px solid ${colors.darkBlueLightMode}`, + }, + "& .MuiTooltip-arrow": { + color: "white", + fontSize: "28px", + "&:before": { + border: `2px solid ${colors.darkBlueLightMode}`, + }, + }, +})); + +const tooltipStyle = { + container: { + background: "white", + }, + icon: { + color: "#9157FF", + fontSize: "20px", + }, +}; + +interface TooltipStatelessProps extends Omit { + title: string; + content: ReactNode; + handleOpen?: (value: boolean) => void; + handleClose?: () => void; +} + +export const TooltipModal = ({ + title, + content, + children, + placement, + open, + handleClose, +}: TooltipStatelessProps) => { + const isMobile = useMediaQuery((theme: Theme) => + theme.breakpoints.down("sm"), + ); + return ( + + + {title} + + + {content} + + + } + > + {children} + + ); +}; diff --git a/ui-next/src/components/v1/TooltipStateless.tsx b/ui-next/src/components/v1/TooltipStateless.tsx new file mode 100644 index 0000000000..c0ff07bc2f --- /dev/null +++ b/ui-next/src/components/v1/TooltipStateless.tsx @@ -0,0 +1,85 @@ +import { Box, Tooltip, TooltipProps, styled } from "@mui/material"; +import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; +import MuiTypography from "components/MuiTypography"; +import { greyText } from "theme/tokens/colors"; +import { ReactNode } from "react"; + +const CustomisedTooltip = styled(({ className, ...props }: TooltipProps) => ( + +))(() => ({ + "& .MuiTooltip-tooltip": { + backgroundColor: "white", + color: "rgba(6, 6, 6, 1)", + maxWidth: 450, + filter: "drop-shadow(0px 0px 6px rgba(89, 89, 89, 0.41))", + borderRadius: "6px", + }, + "& .MuiTooltip-arrow": { + color: "white", + fontSize: "28px", + }, +})); + +const tooltipStyle = { + container: { + background: "white", + padding: "10px 30px 5px 30px", + }, + icon: { + color: "#9157FF", + fontSize: "20px", + }, +}; + +interface TooltipStatelessProps extends Omit { + title: string; + content: ReactNode; + handleOpen: (value: boolean) => void; + handleClose: () => void; +} + +const TooltipStateless = ({ + title, + content, + children, + placement, + open, + handleOpen, + handleClose, +}: TooltipStatelessProps) => { + return ( + handleOpen(true)} + onClose={handleClose} + TransitionProps={{ timeout: 500 }} + title={ + + + + + + {title} + +
    + {content} +
    +
    + } + > + {children} +
    + ); +}; + +export type { TooltipStatelessProps }; +export default TooltipStateless; diff --git a/ui-next/src/components/v1/UnderlinedText.tsx b/ui-next/src/components/v1/UnderlinedText.tsx new file mode 100644 index 0000000000..950820de8e --- /dev/null +++ b/ui-next/src/components/v1/UnderlinedText.tsx @@ -0,0 +1,40 @@ +import { Typography } from "components"; +import { SxProps } from "@mui/system"; + +type UnderlinedTextProps = { + text: string; + underlinedIndexes: number[]; +}; + +const underlinedStyle: SxProps = { + fontSize: "inherit", + fontWeight: "inherit", + textDecoration: "underline", + textUnderlineOffset: "0.2em", +}; + +export const UnderlinedText = ({ + text, + underlinedIndexes, +}: UnderlinedTextProps) => { + return ( + + {text.split("").map((char, index) => + underlinedIndexes.includes(index) ? ( + + {char} + + ) : ( + char + ), + )} + + ); +}; diff --git a/ui-next/src/components/v1/date-time/ConductorDateRangePicker.tsx b/ui-next/src/components/v1/date-time/ConductorDateRangePicker.tsx new file mode 100644 index 0000000000..22eaf38653 --- /dev/null +++ b/ui-next/src/components/v1/date-time/ConductorDateRangePicker.tsx @@ -0,0 +1,91 @@ +import { Grid, SxProps } from "@mui/material"; +import ConductorDateTimePicker from "components/v1/date-time/ConductorDateTimePicker"; +import { ConductorTooltipProps } from "components/conductorTooltip/ConductorTooltip"; +import { ReactNode } from "react"; + +export interface ConductorDateRangePickerProps { + disabled?: boolean; + error?: boolean; + from: Date | null; + helperTextFrom?: ReactNode; + helperTextTo?: ReactNode; + inputSx?: SxProps; + labelFrom?: string; + labelTo?: string; + onFromChange: (val: any) => void; + onToChange: (val: any) => void; + sx?: SxProps; + to: Date | null; + tooltipTo?: Omit; + tooltipFrom?: Omit; +} + +const ConductorDateRangePicker = ({ + disabled, + error, + from, + helperTextFrom, + helperTextTo, + inputSx, + labelFrom, + labelTo, + onFromChange, + onToChange, + sx, + to, + tooltipTo, + tooltipFrom, +}: ConductorDateRangePickerProps) => { + return ( + + + + + + + + + ); +}; + +export default ConductorDateRangePicker; diff --git a/ui-next/src/components/v1/date-time/ConductorDateTimePicker.tsx b/ui-next/src/components/v1/date-time/ConductorDateTimePicker.tsx new file mode 100644 index 0000000000..32e62cb8c1 --- /dev/null +++ b/ui-next/src/components/v1/date-time/ConductorDateTimePicker.tsx @@ -0,0 +1,60 @@ +import { + DateTimePicker, + DateTimePickerProps, + LocalizationProvider, +} from "@mui/x-date-pickers"; +import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns"; +import React, { RefAttributes, forwardRef } from "react"; +import { TextFieldProps } from "@mui/material"; + +import ConductorInput, { + ConductorInputProps, +} from "components/v1/ConductorInput"; +import DatetimeIcon from "components/v1/icons/DatetimeIcon"; +import { FORMAT_DATE_TIME_PICKER } from "utils/constants/common"; + +export type ConductorDateTimePickerProps = DateTimePickerProps & + RefAttributes & { + inputProps?: ConductorInputProps; + }; + +const ConductorDateTimePicker = forwardRef< + HTMLInputElement, + ConductorDateTimePickerProps +>( + ( + { + autoFocus, + format = FORMAT_DATE_TIME_PICKER, + disabled, + inputProps, + ...restProps + }, + ref, + ) => { + return ( + + , + }} + slotProps={{ + textField: { ...inputProps, disabled }, + actionBar: { + actions: ["clear", "accept"], + }, + }} + /> + + ); + }, +); + +export default ConductorDateTimePicker; diff --git a/ui-next/src/components/v1/date-time/ConductorSingleDateRangePicker.tsx b/ui-next/src/components/v1/date-time/ConductorSingleDateRangePicker.tsx new file mode 100644 index 0000000000..7671b10df7 --- /dev/null +++ b/ui-next/src/components/v1/date-time/ConductorSingleDateRangePicker.tsx @@ -0,0 +1,75 @@ +import { Box } from "@mui/material"; +import { ReactNode, useState } from "react"; +import DatePicker from "react-datepicker"; +import "react-datepicker/dist/react-datepicker.css"; +import { getEndOfDayTime, getStartOfDayTime } from "utils/date"; +import "./CustomDateRangePicker.scss"; + +export interface SingleDateRangePickerProps { + fromDate: string; + toDate: string; + setStartTime: (data: string) => void; + setEndTime: (data: string) => void; + maxDate?: boolean; +} + +export const SingleDateRangePicker = ({ + fromDate, + toDate, + setStartTime, + setEndTime, + maxDate, +}: SingleDateRangePickerProps) => { + const [localStartDate, setLocalStartDate] = useState( + fromDate ? new Date(Number(fromDate)) : null, + ); + const [localEndDate, setLocalEndDate] = useState( + fromDate && toDate ? new Date(Number(toDate)) : null, + ); + + const onChange = (dates: [Date | null, Date | null] | null) => { + if (dates) { + const [start, end] = dates; + setLocalStartDate(start); + setLocalEndDate(end); + + const startTimeStamp = String(getStartOfDayTime(start)); + setStartTime(startTimeStamp); + + if (end) { + const endTimeStamp = String(getEndOfDayTime(end)); + setEndTime(endTimeStamp); + } else { + // if there's no end, it means that the range selection has re-started, + // meaning it's the first click of the 2-clicks process + const endTimeStamp = String(getEndOfDayTime(start)); + setEndTime(endTimeStamp); + } + } + }; + + const formatWeekDay = (day: string): ReactNode => { + return day.charAt(0); + }; + + return ( + + + + ); +}; diff --git a/ui-next/src/components/v1/date-time/ConductorTimePicker.tsx b/ui-next/src/components/v1/date-time/ConductorTimePicker.tsx new file mode 100644 index 0000000000..8b441135a7 --- /dev/null +++ b/ui-next/src/components/v1/date-time/ConductorTimePicker.tsx @@ -0,0 +1,60 @@ +import { Box, SxProps, TextFieldProps } from "@mui/material"; +import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns"; +import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; +import { TimePicker } from "@mui/x-date-pickers/TimePicker"; +import ConductorInput from "components/v1/ConductorInput"; +import React from "react"; + +export interface ConductorTimePickerProps { + id?: string; + timeValue: string; + label: string; + sx?: SxProps; + updateTime: (data: string) => void; + error?: string; +} + +export const ConductorTimePicker = ({ + id = "timepicker-time", + label, + timeValue, + sx, + updateTime, + error, + ...restProps +}: ConductorTimePickerProps) => { + const time = timeValue ? new Date(Number(timeValue)) : new Date(); + + const handleTimeChange = (newValue: Date | null) => { + if (newValue) updateTime(String(newValue?.valueOf())); + }; + + return ( + + + handleTimeChange(newValue)} + views={["hours", "minutes", "seconds"]} + slots={{ + textField: ConductorInput as React.ComponentType, + }} + sx={{ + ...sx, + "& .MuiInputAdornment-root": { + display: "none", + }, + "& fieldset": { + borderColor: error ? "#d6413a !important" : "#AFAFAF", + }, + "& label": { + color: error ? "#d6413a" : "#494949", + }, + }} + /> + + + ); +}; diff --git a/ui-next/src/components/v1/date-time/CustomDateRangePicker.scss b/ui-next/src/components/v1/date-time/CustomDateRangePicker.scss new file mode 100644 index 0000000000..ee6b12cd56 --- /dev/null +++ b/ui-next/src/components/v1/date-time/CustomDateRangePicker.scss @@ -0,0 +1,146 @@ +.react-datepicker { + border: inherit; + font-family: inherit; + + .react-datepicker__navigation { + margin-top: 16px; + margin-bottom: 8px; + top: 7px; + + &--previous { + right: 25px; + left: inherit; + padding-right: 5px; + padding-top: 2px; + } + + &--next { + right: 0; + padding-left: 5px; + padding-top: 2px; + } + + &-icon--next::before, + &-icon--previous::before { + border-color: rgba(5, 5, 5, 0.7); + border-width: 2px 2px 0 0; + height: 0.45rem; + width: 0.45rem; + } + + &:hover *::before { + border-color: inherit; + } + + &:hover { + background-color: rgba(0, 0, 0, 0.04); + border-radius: 50%; + } + } + + .react-datepicker__month-container { + .react-datepicker__header { + background-color: transparent; + border-bottom: inherit; + + .react-datepicker__day-names { + align-items: center; + justify-content: center; + display: flex; + + .react-datepicker__day-name { + font-size: 13px; + line-height: 1.5; + font-weight: 300; + width: 36px; + height: 40px; + margin: 0 2px; + text-align: center; + display: flex; + justify-content: center; + align-items: center; + color: rgba(5, 5, 5, 0.4); + } + } + } + + .react-datepicker__current-month { + padding-left: 24px; + padding-right: 12px; + max-height: 30px; + min-height: 30px; + margin-top: 16px; + margin-bottom: 8px; + font-size: 13px; + line-height: 1.5; + font-weight: 400; + padding-top: 4px; + cursor: pointer; + display: flex; + } + } +} + +.react-datepicker__day { + font-weight: 300; + width: 36px; + height: 36px; + border-radius: 50%; + transform: scale(1.1); + padding-top: 7px; + line-height: 1.5; + border: 1px solid transparent; + + &:hover { + width: 36px; + height: 36px; + border-radius: 50%; + border-color: rgba(5, 5, 5, 0.4); + background-color: rgba(25, 118, 210, 0.04); + } +} + +.react-datepicker__day--today { + border-radius: 50%; + border: 1px solid rgba(5, 5, 5, 0.4) !important; +} + +.react-datepicker__day--outside-month { + visibility: hidden; +} + +.react-datepicker__day--keyboard-selected { + background-color: inherit; +} + +.react-datepicker__day--in-selecting-range { + background-color: transparent !important; + color: #050505; + border: 1px dashed rgba(5, 5, 5, 0.1); +} + +.react-datepicker__day--in-range { + background-color: #e3eefa; + color: #050505; + + &:hover { + border: 1px solid transparent; + background-color: rgba(25, 118, 210, 0.2); + } +} + +.react-datepicker__day--selected, +.react-datepicker__day--range-end { + color: #ffffff !important; + background-color: #1976d2 !important; + font-weight: 400 !important; +} + +.react-datepicker__day--selected:hover, +.react-datepicker__day--range-end:hover { + background-color: #30499f !important; +} + +.react-datepicker__day--disabled { + color: #ccc !important; +} diff --git a/ui-next/src/components/v1/error/Error.tsx b/ui-next/src/components/v1/error/Error.tsx new file mode 100644 index 0000000000..cb3b9b2c5a --- /dev/null +++ b/ui-next/src/components/v1/error/Error.tsx @@ -0,0 +1,118 @@ +import Box from "@mui/material/Box"; +import Chip from "@mui/material/Chip"; +import { Theme } from "@mui/material/styles"; +import MuiButton from "components/MuiButton"; +import MuiTypography from "components/MuiTypography"; +import { useNavigate } from "react-router"; +import { HttpStatusCode } from "utils/constants/httpStatusCode"; +import { clear as clearTokens } from "shared/auth/tokenManagerJotai"; + +export interface ErrorProps { + title: string; + description: string; + buttonText?: string; + onClick?: () => void; + errorLogo?: string; + error?: string; + secondaryButton?: { + buttonText?: string; + onClick?: () => void; + }; +} + +export default function Error({ + title, + description, + buttonText = "GO BACK", + onClick, + error, +}: ErrorProps) { + const navigate = useNavigate(); + + const handleClick = () => { + if (onClick) { + onClick(); + return; + } + + if (error === "INVALID_TOKEN") { + // Clear all auth-related storage using token manager + clearTokens(); // Clears tokens from memory and localStorage + sessionStorage.clear(); // Clear OIDC state to prevent auth loop + + // Force reload to login page to ensure clean state + window.location.href = "/"; + return; + } + + // If there's no previous history, go to home + if (navigate.length <= 1) { + navigate("/"); + } else { + navigate(-1); + } + }; + + return ( + + HttpStatusCode.BadRequest ? 200 : 50} + fontWeight={700} + textAlign="center" + mt={20} + mb={5} + > + {title} + + + + ({ + background: theme.palette.background.paper, + display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "center", + minHeight: "150px", + width: "340px", + py: 2, + px: 4, + gap: 3, + borderRadius: "6px", + })} + > + theme.palette.pink.main, + padding: "7px", + borderRadius: "100px", + fontWeight: 500, + fontSize: "12px", + }} + label={"Error"} + /> + + + {description} + + theme.palette.primary.main, + }} + > + {buttonText} + + + + + ); +} diff --git a/ui-next/src/components/v1/icons/AddIcon.tsx b/ui-next/src/components/v1/icons/AddIcon.tsx new file mode 100644 index 0000000000..db75cc0186 --- /dev/null +++ b/ui-next/src/components/v1/icons/AddIcon.tsx @@ -0,0 +1,17 @@ +const AddIcon = (props: any) => ( + + + +); + +export default AddIcon; diff --git a/ui-next/src/components/v1/icons/AnnouncementIcon.tsx b/ui-next/src/components/v1/icons/AnnouncementIcon.tsx new file mode 100644 index 0000000000..727ba9930a --- /dev/null +++ b/ui-next/src/components/v1/icons/AnnouncementIcon.tsx @@ -0,0 +1,16 @@ +const AnnouncementIcon = ({ size = 21, color = "#ffffff" }) => ( + + + +); + +export default AnnouncementIcon; diff --git a/ui-next/src/components/v1/icons/ArrowDownIcon.tsx b/ui-next/src/components/v1/icons/ArrowDownIcon.tsx new file mode 100644 index 0000000000..79f5cb3698 --- /dev/null +++ b/ui-next/src/components/v1/icons/ArrowDownIcon.tsx @@ -0,0 +1,18 @@ +const ArrowDownIcon = ({ size = "20", color = "#000000" }) => { + return ( + + + + ); +}; + +export default ArrowDownIcon; diff --git a/ui-next/src/components/v1/icons/ArrowUpIcon.tsx b/ui-next/src/components/v1/icons/ArrowUpIcon.tsx new file mode 100644 index 0000000000..23079ca2d4 --- /dev/null +++ b/ui-next/src/components/v1/icons/ArrowUpIcon.tsx @@ -0,0 +1,18 @@ +const ArrowUpIcon = ({ size = "20", color = "#000000" }) => { + return ( + + + + ); +}; + +export default ArrowUpIcon; diff --git a/ui-next/src/components/v1/icons/ChatIcon.tsx b/ui-next/src/components/v1/icons/ChatIcon.tsx new file mode 100644 index 0000000000..bde156bf4e --- /dev/null +++ b/ui-next/src/components/v1/icons/ChatIcon.tsx @@ -0,0 +1,16 @@ +const ChatIcon = ({ size = 21, color = "#ffffff" }) => ( + + + +); + +export default ChatIcon; diff --git a/ui-next/src/components/v1/icons/CircleCheckIcon.tsx b/ui-next/src/components/v1/icons/CircleCheckIcon.tsx new file mode 100644 index 0000000000..e73b49243f --- /dev/null +++ b/ui-next/src/components/v1/icons/CircleCheckIcon.tsx @@ -0,0 +1,25 @@ +const CircleCheckIcon = (props: any) => ( + + + + + + + + + + + +); + +export default CircleCheckIcon; diff --git a/ui-next/src/components/v1/icons/CopyIcon.tsx b/ui-next/src/components/v1/icons/CopyIcon.tsx new file mode 100644 index 0000000000..966a9d8e5a --- /dev/null +++ b/ui-next/src/components/v1/icons/CopyIcon.tsx @@ -0,0 +1,17 @@ +const CopyIcon = ({ size = 20, ...props }: any) => ( + + + +); + +export default CopyIcon; diff --git a/ui-next/src/components/v1/icons/DatetimeIcon.tsx b/ui-next/src/components/v1/icons/DatetimeIcon.tsx new file mode 100644 index 0000000000..467a364c6e --- /dev/null +++ b/ui-next/src/components/v1/icons/DatetimeIcon.tsx @@ -0,0 +1,19 @@ +import { CustomIconType } from "components/flow/components/shapes/TaskCard/icons/types"; + +const DatetimeIcon = ({ size = 20, ...props }: CustomIconType) => ( + + + +); + +export default DatetimeIcon; diff --git a/ui-next/src/components/v1/icons/DocsIcon.tsx b/ui-next/src/components/v1/icons/DocsIcon.tsx new file mode 100644 index 0000000000..f19d6511c2 --- /dev/null +++ b/ui-next/src/components/v1/icons/DocsIcon.tsx @@ -0,0 +1,17 @@ +const DocsIcon = (props: any) => ( + + + +); + +export default DocsIcon; diff --git a/ui-next/src/components/v1/icons/DoubleArrowLeftIcon.tsx b/ui-next/src/components/v1/icons/DoubleArrowLeftIcon.tsx new file mode 100644 index 0000000000..5514f5f858 --- /dev/null +++ b/ui-next/src/components/v1/icons/DoubleArrowLeftIcon.tsx @@ -0,0 +1,21 @@ +const DoubleArrowLeftIcon = (props: any) => ( + + + + + +); + +export default DoubleArrowLeftIcon; diff --git a/ui-next/src/components/v1/icons/DoubleArrowRightIcon.tsx b/ui-next/src/components/v1/icons/DoubleArrowRightIcon.tsx new file mode 100644 index 0000000000..706e883d15 --- /dev/null +++ b/ui-next/src/components/v1/icons/DoubleArrowRightIcon.tsx @@ -0,0 +1,21 @@ +const DoubleArrowRightIcon = (props: any) => ( + + + + + +); + +export default DoubleArrowRightIcon; diff --git a/ui-next/src/components/v1/icons/DownloadIcon.tsx b/ui-next/src/components/v1/icons/DownloadIcon.tsx new file mode 100644 index 0000000000..8a203691ed --- /dev/null +++ b/ui-next/src/components/v1/icons/DownloadIcon.tsx @@ -0,0 +1,17 @@ +const DownloadIcon = (props: any) => ( + + + +); + +export default DownloadIcon; diff --git a/ui-next/src/components/v1/icons/DropdownIcon.tsx b/ui-next/src/components/v1/icons/DropdownIcon.tsx new file mode 100644 index 0000000000..1922825491 --- /dev/null +++ b/ui-next/src/components/v1/icons/DropdownIcon.tsx @@ -0,0 +1,17 @@ +const DropdownIcon = (props: any) => ( + + + +); + +export default DropdownIcon; diff --git a/ui-next/src/components/v1/icons/EnterIcon.tsx b/ui-next/src/components/v1/icons/EnterIcon.tsx new file mode 100644 index 0000000000..0318d7b16a --- /dev/null +++ b/ui-next/src/components/v1/icons/EnterIcon.tsx @@ -0,0 +1,18 @@ +const EnterIcon = ({ size = 21, color = "#ffffff" }) => ( + + + +); + +export default EnterIcon; diff --git a/ui-next/src/components/v1/icons/ExitIcon.tsx b/ui-next/src/components/v1/icons/ExitIcon.tsx new file mode 100644 index 0000000000..fb58947e0a --- /dev/null +++ b/ui-next/src/components/v1/icons/ExitIcon.tsx @@ -0,0 +1,29 @@ +const ExitIcon = (props: any) => ( + + + + + + + + + + +); + +export default ExitIcon; diff --git a/ui-next/src/components/v1/icons/ExpandIcon.tsx b/ui-next/src/components/v1/icons/ExpandIcon.tsx new file mode 100644 index 0000000000..eb08c0483a --- /dev/null +++ b/ui-next/src/components/v1/icons/ExpandIcon.tsx @@ -0,0 +1,42 @@ +const ExpandIcon = ({ size = 21, color = "#ffffff" }) => ( + + + + + + + +); + +export default ExpandIcon; diff --git a/ui-next/src/components/v1/icons/FilterIcon.tsx b/ui-next/src/components/v1/icons/FilterIcon.tsx new file mode 100644 index 0000000000..f05b130f00 --- /dev/null +++ b/ui-next/src/components/v1/icons/FilterIcon.tsx @@ -0,0 +1,17 @@ +const FilterIcon = (props: any) => ( + + + +); + +export default FilterIcon; diff --git a/ui-next/src/components/v1/icons/InfoIcon.tsx b/ui-next/src/components/v1/icons/InfoIcon.tsx new file mode 100644 index 0000000000..8d0f10ba2c --- /dev/null +++ b/ui-next/src/components/v1/icons/InfoIcon.tsx @@ -0,0 +1,17 @@ +const InfoIcon = ({ size, ...props }: any) => ( + + + +); + +export default InfoIcon; diff --git a/ui-next/src/components/v1/icons/NewIntegration.tsx b/ui-next/src/components/v1/icons/NewIntegration.tsx new file mode 100644 index 0000000000..bf289ff231 --- /dev/null +++ b/ui-next/src/components/v1/icons/NewIntegration.tsx @@ -0,0 +1,16 @@ +const NewIntegrationIcon = () => ( + + + +); + +export default NewIntegrationIcon; diff --git a/ui-next/src/components/v1/icons/OpenIcon.tsx b/ui-next/src/components/v1/icons/OpenIcon.tsx new file mode 100644 index 0000000000..8221a183c4 --- /dev/null +++ b/ui-next/src/components/v1/icons/OpenIcon.tsx @@ -0,0 +1,26 @@ +import { CustomIconType } from "components/flow/components/shapes/TaskCard/icons/types"; + +const OpenIcon = ({ size = 16, ...props }: CustomIconType) => ( + + + + + + + + + + +); + +export default OpenIcon; diff --git a/ui-next/src/components/v1/icons/PlayIcon.tsx b/ui-next/src/components/v1/icons/PlayIcon.tsx new file mode 100644 index 0000000000..5d5b58291d --- /dev/null +++ b/ui-next/src/components/v1/icons/PlayIcon.tsx @@ -0,0 +1,19 @@ +import { CustomIconType } from "components/flow/components/shapes/TaskCard/icons/types"; + +const PlayIcon = ({ size = 20, ...props }: CustomIconType) => ( + + + +); + +export default PlayIcon; diff --git a/ui-next/src/components/v1/icons/PythonIcon.tsx b/ui-next/src/components/v1/icons/PythonIcon.tsx new file mode 100644 index 0000000000..0d88a3e221 --- /dev/null +++ b/ui-next/src/components/v1/icons/PythonIcon.tsx @@ -0,0 +1,56 @@ +const PythonIcon = ({ size = 24 }: { size?: number }) => ( + + + + + + + + + + + + + + + + + +); + +export default PythonIcon; diff --git a/ui-next/src/components/v1/icons/RefreshIcon.tsx b/ui-next/src/components/v1/icons/RefreshIcon.tsx new file mode 100644 index 0000000000..bc75da1dd2 --- /dev/null +++ b/ui-next/src/components/v1/icons/RefreshIcon.tsx @@ -0,0 +1,31 @@ +import { CustomIconType } from "components/flow/components/shapes/TaskCard/icons/types"; + +const RefreshIcon = ({ size = 20, ...props }: CustomIconType) => ( + + + + + + + + + + +); + +export default RefreshIcon; diff --git a/ui-next/src/components/v1/icons/RequestACallIcon.tsx b/ui-next/src/components/v1/icons/RequestACallIcon.tsx new file mode 100644 index 0000000000..94665bc3f0 --- /dev/null +++ b/ui-next/src/components/v1/icons/RequestACallIcon.tsx @@ -0,0 +1,17 @@ +const RequestACallIcon = (props: any) => ( + + + +); + +export default RequestACallIcon; diff --git a/ui-next/src/components/v1/icons/ResetIcon.tsx b/ui-next/src/components/v1/icons/ResetIcon.tsx new file mode 100644 index 0000000000..fa08ef27b8 --- /dev/null +++ b/ui-next/src/components/v1/icons/ResetIcon.tsx @@ -0,0 +1,17 @@ +const ResetIcon = (props: any) => ( + + + +); + +export default ResetIcon; diff --git a/ui-next/src/components/v1/icons/RunIcon.tsx b/ui-next/src/components/v1/icons/RunIcon.tsx new file mode 100644 index 0000000000..9aad13089d --- /dev/null +++ b/ui-next/src/components/v1/icons/RunIcon.tsx @@ -0,0 +1,23 @@ +const RunIcon = (props: any) => ( + + + + + + +); + +export default RunIcon; diff --git a/ui-next/src/components/v1/icons/Save.tsx b/ui-next/src/components/v1/icons/Save.tsx new file mode 100644 index 0000000000..30b791e296 --- /dev/null +++ b/ui-next/src/components/v1/icons/Save.tsx @@ -0,0 +1,17 @@ +const SaveIcon = (props: any) => ( + + + +); + +export default SaveIcon; diff --git a/ui-next/src/components/v1/icons/SaveIcon.tsx b/ui-next/src/components/v1/icons/SaveIcon.tsx new file mode 100644 index 0000000000..30b791e296 --- /dev/null +++ b/ui-next/src/components/v1/icons/SaveIcon.tsx @@ -0,0 +1,17 @@ +const SaveIcon = (props: any) => ( + + + +); + +export default SaveIcon; diff --git a/ui-next/src/components/v1/icons/SearchIcon.tsx b/ui-next/src/components/v1/icons/SearchIcon.tsx new file mode 100644 index 0000000000..00f610feff --- /dev/null +++ b/ui-next/src/components/v1/icons/SearchIcon.tsx @@ -0,0 +1,17 @@ +const SearchIcon = (props: any) => ( + + + +); + +export default SearchIcon; diff --git a/ui-next/src/components/v1/icons/ShowViewIcon.tsx b/ui-next/src/components/v1/icons/ShowViewIcon.tsx new file mode 100644 index 0000000000..45810594a0 --- /dev/null +++ b/ui-next/src/components/v1/icons/ShowViewIcon.tsx @@ -0,0 +1,17 @@ +const ShowViewIcon = (props: any) => ( + + + +); + +export default ShowViewIcon; diff --git a/ui-next/src/components/v1/icons/TestIcon.tsx b/ui-next/src/components/v1/icons/TestIcon.tsx new file mode 100644 index 0000000000..e5128a8979 --- /dev/null +++ b/ui-next/src/components/v1/icons/TestIcon.tsx @@ -0,0 +1,26 @@ +import { CustomIconType } from "components/flow/components/shapes/TaskCard/icons/types"; + +const TestIcon = ({ size = 20, ...props }: CustomIconType) => ( + + + + + + + + + + +); + +export default TestIcon; diff --git a/ui-next/src/components/v1/icons/TimeIcon.tsx b/ui-next/src/components/v1/icons/TimeIcon.tsx new file mode 100644 index 0000000000..4ce71b04f0 --- /dev/null +++ b/ui-next/src/components/v1/icons/TimeIcon.tsx @@ -0,0 +1,19 @@ +import { CustomIconType } from "components/flow/components/shapes/TaskCard/icons/types"; + +const TimeIcon = ({ size = 20, ...props }: CustomIconType) => ( + + + +); + +export default TimeIcon; diff --git a/ui-next/src/components/v1/icons/TrashIcon.tsx b/ui-next/src/components/v1/icons/TrashIcon.tsx new file mode 100644 index 0000000000..f34396feb2 --- /dev/null +++ b/ui-next/src/components/v1/icons/TrashIcon.tsx @@ -0,0 +1,17 @@ +const TrashIcon = (props: any) => ( + + + +); + +export default TrashIcon; diff --git a/ui-next/src/components/v1/icons/UnlockIcon.tsx b/ui-next/src/components/v1/icons/UnlockIcon.tsx new file mode 100644 index 0000000000..e77d882238 --- /dev/null +++ b/ui-next/src/components/v1/icons/UnlockIcon.tsx @@ -0,0 +1,16 @@ +const UnlockIcon = ({ size = 21, color = "#ffffff" }) => ( + + + +); + +export default UnlockIcon; diff --git a/ui-next/src/components/v1/icons/XCloseIcon.tsx b/ui-next/src/components/v1/icons/XCloseIcon.tsx new file mode 100644 index 0000000000..e6b9efa4a0 --- /dev/null +++ b/ui-next/src/components/v1/icons/XCloseIcon.tsx @@ -0,0 +1,19 @@ +import { CustomIconType } from "components/flow/components/shapes/TaskCard/icons/types"; + +const XCloseIcon = ({ size = 20, ...props }: CustomIconType) => ( + + + +); + +export default XCloseIcon; diff --git a/ui-next/src/components/v1/index.ts b/ui-next/src/components/v1/index.ts new file mode 100644 index 0000000000..6ec62b5f68 --- /dev/null +++ b/ui-next/src/components/v1/index.ts @@ -0,0 +1,2 @@ +export * from "./ConductorAutoComplete"; +export * from "./ConductorSelect"; diff --git a/ui-next/src/components/v1/layout/MessageContext/MessageContext.tsx b/ui-next/src/components/v1/layout/MessageContext/MessageContext.tsx new file mode 100644 index 0000000000..516299af08 --- /dev/null +++ b/ui-next/src/components/v1/layout/MessageContext/MessageContext.tsx @@ -0,0 +1,10 @@ +import { createContext } from "react"; +import { PopoverMessage } from "types/Messages"; + +type MessageState = { + setMessage: (msg: PopoverMessage | null) => void; +}; + +export const MessageContext = createContext({ + setMessage: () => null, +}); diff --git a/ui-next/src/components/v1/layout/MessageContext/MessageProvider.tsx b/ui-next/src/components/v1/layout/MessageContext/MessageProvider.tsx new file mode 100644 index 0000000000..475baafc0a --- /dev/null +++ b/ui-next/src/components/v1/layout/MessageContext/MessageProvider.tsx @@ -0,0 +1,31 @@ +import { SnackbarMessage } from "components/SnackbarMessage"; +import { ReactNode, useState } from "react"; +import { PopoverMessage } from "types/Messages"; +import { MessageContext } from "./MessageContext"; + +const defaultMessage = null; + +interface MessageProviderProps { + children?: ReactNode; +} + +export const MessageProvider = ({ children }: MessageProviderProps) => { + const [message, setMessage] = useState(defaultMessage); + + return ( + <> + {message ? ( + setMessage(defaultMessage)} + /> + ) : null} + + + {children} + + + ); +}; diff --git a/ui-next/src/components/v1/layout/MessageContext/index.ts b/ui-next/src/components/v1/layout/MessageContext/index.ts new file mode 100644 index 0000000000..e8659ec0e7 --- /dev/null +++ b/ui-next/src/components/v1/layout/MessageContext/index.ts @@ -0,0 +1,2 @@ +export * from "./MessageContext"; +export * from "./MessageProvider"; diff --git a/ui-next/src/components/v1/layout/header/AnnouncementBanner.tsx b/ui-next/src/components/v1/layout/header/AnnouncementBanner.tsx new file mode 100644 index 0000000000..f4204515a0 --- /dev/null +++ b/ui-next/src/components/v1/layout/header/AnnouncementBanner.tsx @@ -0,0 +1,245 @@ +import HighlightOffOutlinedIcon from "@mui/icons-material/HighlightOffOutlined"; +import Box, { BoxProps } from "@mui/material/Box"; +import Link from "@mui/material/Link"; +import MuiIconButton from "components/MuiIconButton"; +import { drawerWidthClose } from "../../../Sidebar/constants"; +import { colors } from "theme/tokens/variables"; +import { useAnnouncementBanner } from "./bannerUtils"; +import AnnouncementIcon from "components/v1/icons/AnnouncementIcon"; +import { Grid } from "@mui/material"; +import Button from "components/MuiButton"; +import ChatIcon from "components/v1/icons/ChatIcon"; +import { openInNewTab } from "utils/helpers"; +import UnlockIcon from "components/v1/icons/UnlockIcon"; +import BannerIcon from "images/svg/banner-icon.svg"; + +const TALK_TO_AN_EXPERT_URL = "https://orkes.io/talk-to-an-expert"; + +export interface AnnouncementBannerProps extends BoxProps { + bannerOpen: boolean; + setBannerOpen: (val: boolean) => void; + trialExpiryDate: number | Date; + isTrialExpired: boolean; + showAiStudioBanner?: boolean; + dismissAiStudioBanner: () => void; + /** Whether the announcement banner has been dismissed */ + isAnnouncementBannerDismissed: boolean; + /** Callback to dismiss the announcement banner */ + onDismissAnnouncementBanner: () => void; +} + +export default function AnnouncementBanner({ + sx, + bannerOpen, + setBannerOpen, + trialExpiryDate, + isTrialExpired, + showAiStudioBanner, + dismissAiStudioBanner, + isAnnouncementBannerDismissed, + onDismissAnnouncementBanner, + ...rest +}: AnnouncementBannerProps) { + const { showBanner, daysToGo } = useAnnouncementBanner( + isTrialExpired, + trialExpiryDate!, + isAnnouncementBannerDismissed, + ); + + const handleBannerDismiss = () => { + setBannerOpen(false); + onDismissAnnouncementBanner(); + }; + + const renderAnnouncementText = () => { + if (daysToGo === 0) { + return `Trial ends today.`; + } + return `Trial ends in ${daysToGo} ${daysToGo === 1 ? "day" : "days"}.`; + }; + + if (isTrialExpired) { + return ( + + + + + + + Trial has expired. + + + Your trial has ended. Please contact sales or upgrade your + cluster. + + + + + + + + + + + + ); + } + if (bannerOpen && showBanner) { + return ( + theme.palette.background.paper, + fontWeight: 700, + fontSize: "14px", + display: "flex", + alignItems: "center", + justifyContent: "center", + "> a": { + ml: 1, + color: colors.sidebarBlacky, + }, + ...sx, + "@media (max-width: 598px)": { + flexDirection: "column", + pl: `${drawerWidthClose + 8}px`, + }, + }} + {...rest} + > + {renderAnnouncementText()} + <> + + Contact us  + + + to upgrade to Enterprise. + handleBannerDismiss()} + > + + + + ); + } + + if (showAiStudioBanner) { + return ( + theme.palette.background.paper, + fontWeight: 700, + fontSize: "14px", + display: "flex", + alignItems: "center", + justifyContent: "center", + "> a": { + textDecoration: "underline", + color: colors.white, + ml: 1, + }, + ...sx, + "@media (max-width: 598px)": { + flexDirection: "column", + pl: `${drawerWidthClose + 8}px`, + }, + }} + {...rest} + > + banner + Extra, extra,{" "} + + read all about it + + ! AI agent creation is coming to Orkes; meaning endless possibilities. + 😘 + <> + + + + + ); + } + return null; +} diff --git a/ui-next/src/components/v1/layout/header/ButtonLinks.tsx b/ui-next/src/components/v1/layout/header/ButtonLinks.tsx new file mode 100644 index 0000000000..92a4ed7164 --- /dev/null +++ b/ui-next/src/components/v1/layout/header/ButtonLinks.tsx @@ -0,0 +1,167 @@ +import MoreVertIcon from "@mui/icons-material/MoreVert"; +import Box, { BoxProps } from "@mui/material/Box"; +import ClickAwayListener from "@mui/material/ClickAwayListener"; +import Grow from "@mui/material/Grow"; +import ListItemIcon from "@mui/material/ListItemIcon"; +import ListItemText from "@mui/material/ListItemText"; +import MenuItem from "@mui/material/MenuItem"; +import MenuList from "@mui/material/MenuList"; +import Paper from "@mui/material/Paper"; +import Popper from "@mui/material/Popper"; +import Stack from "@mui/material/Stack"; +import { Theme } from "@mui/material/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import MuiButton from "components/MuiButton"; +import MuiButtonGroup from "components/MuiButtonGroup"; +import DocsIcon from "components/v1/icons/DocsIcon"; +import SlackIcon from "images/svg/slack-logo-transparent.svg?react"; +import { useRef, useState } from "react"; +import { colors } from "theme/tokens/variables"; +import { featureFlags, FEATURES } from "utils/flags"; +import { openInNewTab } from "utils/helpers"; + +const linkButtons = [ + { + text: "Docs", + icon: , + linkTo: "https://orkes.io/content/", + hidden: !featureFlags.isEnabled(FEATURES.SHOW_DOCUMENTATION), + }, + + { + text: "Join our Slack", + icon: , + linkTo: + "https://join.slack.com/t/orkes-conductor/shared_invite/zt-xyxqyseb-YZ3hwwAgHJH97bsrYRnSZg", + hidden: !featureFlags.isEnabled(FEATURES.SHOW_JOIN_SLACK_COMMUNITY), + }, +]; + +const SMALL_SCREEN_BREAKPOINT_WHEN_SIDEBAR_OPEN = 1200; + +export interface ButtonLinksProps extends BoxProps { + showDropdownOnly: boolean; + isSideBarOpen: boolean; +} + +const buttonsToRender = linkButtons.filter((button) => !button.hidden); + +export default function ButtonLinks({ + showDropdownOnly, + isSideBarOpen, + ...rest +}: ButtonLinksProps) { + const [open, setOpen] = useState(false); + const anchorRef = useRef(null); + // Checking responsive width (Mobile) + const isSmallScreen = useMediaQuery((theme: Theme) => + theme.breakpoints.down( + isSideBarOpen ? SMALL_SCREEN_BREAKPOINT_WHEN_SIDEBAR_OPEN : "md", + ), + ); + + const handleToggle = () => { + setOpen((prevOpen) => !prevOpen); + }; + + const handleClose = (event: Event) => { + if ( + anchorRef.current && + anchorRef.current.contains(event.target as HTMLElement) + ) { + return; + } + + setOpen(false); + }; + return buttonsToRender.length !== 0 ? ( + + + {!isSmallScreen && + !showDropdownOnly && + buttonsToRender.map(({ text, linkTo, icon }) => ( + openInNewTab(linkTo)} + > + {text} + + ))} + {(isSmallScreen || showDropdownOnly) && ( + + } onClick={handleToggle}> + More + + + )} + + {({ TransitionProps, placement }) => ( + + + + + {buttonsToRender.map((option) => ( + { + openInNewTab(option.linkTo); + setOpen(false); + }} + > + + {option.icon} + + {option.text} + + ))} + + + + + )} + + + + ) : null; +} diff --git a/ui-next/src/components/v1/layout/header/bannerUtils.ts b/ui-next/src/components/v1/layout/header/bannerUtils.ts new file mode 100644 index 0000000000..c0e1c16d88 --- /dev/null +++ b/ui-next/src/components/v1/layout/header/bannerUtils.ts @@ -0,0 +1,25 @@ +import { useMemo } from "react"; +import { differenceInDays } from "utils/date"; + +export const currentDate = new Date().setHours(0, 0, 0, 0); + +export const useAnnouncementBanner = ( + isTrialExpired: boolean, + trialExpiryDate: number | Date, + isAnnouncementBannerDismissed: boolean, +) => { + const daysToGo = differenceInDays(trialExpiryDate!, currentDate); + const showBanner = useMemo(() => { + if (isAnnouncementBannerDismissed) { + return false; + } else { + return true; + } + }, [isAnnouncementBannerDismissed]); + + return { + showBanner: + (trialExpiryDate && daysToGo >= 0 && showBanner) || isTrialExpired, + daysToGo, + }; +}; diff --git a/ui-next/src/components/v1/layout/section/ConductorSectionHeader.tsx b/ui-next/src/components/v1/layout/section/ConductorSectionHeader.tsx new file mode 100644 index 0000000000..92c21b6fb8 --- /dev/null +++ b/ui-next/src/components/v1/layout/section/ConductorSectionHeader.tsx @@ -0,0 +1,153 @@ +import { FunctionComponent, ReactNode, useContext } from "react"; +import ConductorBreadcrumbs from "components/v1/ConductorBreadcrumbs"; +import { + Box, + MenuItem, + Select, + Stack, + StackProps, + useMediaQuery, +} from "@mui/material"; +import { Theme } from "@mui/material/styles"; +import Button, { MuiButtonProps } from "components/MuiButton"; +import { SidebarContext } from "components/Sidebar/context/SidebarContext"; + +export interface ActionButton extends MuiButtonProps { + label?: ReactNode; + hidden?: boolean; +} + +const SIDEBAR_OPEN_BREAKPOINT = 800; +const VALID_WIDTH_BREAKPOINT = 491; + +export interface ConductorSectionHeaderProps extends Omit { + title: ReactNode; + id?: string; + versionSelector?: { + current: number; + available: number[]; + onChange: (version: number) => void; + }; + buttons?: ActionButton[]; + buttonsComponent?: ReactNode; + breadcrumbItems?: { + label: string; + to: string; + icon?: ReactNode; + }[]; +} + +export const ConductorSectionHeader: FunctionComponent< + ConductorSectionHeaderProps +> = ({ + id = "conductor-header-section-container", + title, + buttons, + buttonsComponent, + breadcrumbItems, + versionSelector, + ...restProps +}) => { + const { open: isSideBarOpen } = useContext(SidebarContext); + const isValidOuterWidth = useMediaQuery((theme: Theme) => + theme.breakpoints.down( + isSideBarOpen ? SIDEBAR_OPEN_BREAKPOINT : VALID_WIDTH_BREAKPOINT, + ), + ); + const breadcrumbsId = `${id}-breadcrumbs`; + const titleId = `${id}-title`; + const buttonsId = `${id}-buttons`; + + const renderButtons = () => + buttons?.reduce( + ( + result, + { onClick, color, label, disabled, hidden, ...restProps }: ActionButton, + index: number, + ) => { + if (!hidden) { + result.push( + , + ); + } + + return result; + }, + [] as ReactNode[], + ); + + return ( + + + {breadcrumbItems && breadcrumbItems.length > 0 ? ( + + ) : null} + + + {title} + + + + + {versionSelector && ( + + )} + + {buttonsComponent ? buttonsComponent : null} + + {buttons && buttons.length > 0 ? ( + + {renderButtons()} + + ) : null} + + + ); +}; diff --git a/ui-next/src/components/v1/quiz/OnboardingQuiz.tsx b/ui-next/src/components/v1/quiz/OnboardingQuiz.tsx new file mode 100644 index 0000000000..dcb0bf4be8 --- /dev/null +++ b/ui-next/src/components/v1/quiz/OnboardingQuiz.tsx @@ -0,0 +1,311 @@ +import Box from "@mui/material/Box"; +import Grid from "@mui/material/Grid"; +import Modal from "@mui/material/Modal"; +import { CSSObject } from "@mui/material/styles"; +import { useState } from "react"; + +import { Button, Input, Paper, Typography } from "components"; +import RunIcon from "components/v1/icons/RunIcon"; +import CPlusPlusLogo from "images/svg/c-plus-plus-logo.svg"; +import CSharpLogo from "images/svg/c-sharp-logo.svg"; +import GoLangLogo from "images/svg/go-lang-logo.svg"; +import JavaLogo from "images/svg/java-logo.svg"; +import JavaScriptLogo from "images/svg/javascript-logo.svg"; +import PythonLogo from "images/svg/python-logo.svg"; +import { orkesBrandN200, orkesBrandS600 } from "theme/tokens/colors"; +import CircleCheckIcon from "../icons/CircleCheckIcon"; + +const inputStyle = { + "& .MuiInputBase-root": { + minHeight: "auto", + }, + "& .MuiOutlinedInput-notchedOutline": { + border: "none", + }, + "& .MuiInputBase-input": { + p: 0, + }, +}; + +const goals = [ + { id: 1, label: "Evaluating Orkes for my company" }, + { id: 2, label: "Learn about the features and functionalities" }, + { id: 3, label: "Build an application for a use case" }, + { + id: 4, + label: ( + + Other - please specify + + + ), + }, +]; + +const purposes = [ + { id: 1, label: "Microservices based applications" }, + { id: 2, label: "Data pipelines" }, + { id: 3, label: "Gen-AI powered workflows" }, + { + id: 4, + label: ( + + Other - please specify + + + ), + }, +]; + +const languages = [ + { + id: 1, + label: "Java", + logo: JavaLogo, + }, + { + id: 2, + label: "Python", + logo: PythonLogo, + }, + { + id: 3, + label: "C Sharp", + logo: CSharpLogo, + }, + { + id: 4, + label: "C ++", + logo: CPlusPlusLogo, + }, + { + id: 5, + label: "GoLang", + logo: GoLangLogo, + }, + { + id: 6, + label: "JavaScript", + logo: JavaScriptLogo, + }, +]; + +const paperStyle: CSSObject = { + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + p: "20px 40px", + overflow: "auto", + maxHeight: "95%", + outline: "none", +}; + +const itemStyle: CSSObject = { + position: "relative", + display: "flex", + alignItems: "center", + justifyContent: "space-between", + cursor: "pointer", + border: `1px solid ${orkesBrandN200}`, + borderRadius: "6px", + p: "5px 13px", + width: "100%", + height: "100%", + gap: "5px", +}; + +const selectedStyle: CSSObject = { + borderColor: orkesBrandS600, + borderWidth: "2px", +}; + +const titleStyle: CSSObject = { + fontSize: "20px", + fontWeight: 700, + my: 1, +}; + +export default function OnboardingQuiz() { + const [open, setOpen] = useState(true); + const [selectedGoals, setSelectedGoals] = useState([]); + const [selectedPurposes, setSelectedPurposes] = useState([]); + const [selectedLanguages, setSelectedLanguages] = useState([]); + + const isValid = selectedGoals && selectedPurposes && selectedLanguages; + + const handleState = (currentState: number[], selectedItem: number) => { + const result = [...currentState]; + const selectedIndex = result.findIndex((item) => item === selectedItem); + + if (selectedIndex > -1) { + result.splice(selectedIndex, 1); + } else { + result.push(selectedItem); + } + + return result; + }; + + return ( + + + + Help us jump start your work + + + + + WHAT IS YOUR GOAL? + + + {goals.map(({ id, label }) => { + const isActive = selectedGoals.includes(id); + + return ( + + + setSelectedGoals((currentState) => + handleState(currentState, id), + ) + } + > + {label} + {isActive && } + + + ); + })} + + + + + + WHAT ARE YOU LOOKING TO BUILD WITH ORKES? + + + + {purposes.map(({ id, label }) => { + const isActive = selectedPurposes.includes(id); + + return ( + + + setSelectedPurposes((currentState) => + handleState(currentState, id), + ) + } + > + {label} + {isActive && } + + + ); + })} + + + + + + WHAT IS YOUR PREFERRED LANGUAGE FOR CODING? + + + + {languages.map(({ id, label, logo }) => { + const isActive = selectedLanguages.includes(id); + + return ( + + + setSelectedLanguages((currentState) => + handleState(currentState, id), + ) + } + > + {label} + {label} + {isActive && ( + + )} + + + ); + })} + + + + setSelectedLanguages((currentState) => + handleState(currentState, 7), + ) + } + > + + Other - please specify + + + {selectedLanguages.includes(7) && ( + + )} + + + + + + + + + + ); +} diff --git a/ui-next/src/components/v1/react-hook-form/ReactHookFormCheckbox.test.tsx b/ui-next/src/components/v1/react-hook-form/ReactHookFormCheckbox.test.tsx new file mode 100644 index 0000000000..d9db43d723 --- /dev/null +++ b/ui-next/src/components/v1/react-hook-form/ReactHookFormCheckbox.test.tsx @@ -0,0 +1,54 @@ +import "@testing-library/jest-dom"; +import { fireEvent, render, screen } from "@testing-library/react"; +import { useForm } from "react-hook-form"; +import ReactHookFormCheckbox from "./ReactHookFormCheckbox"; + +describe("ReactHookFormCheckbox", () => { + const setup = (props: any = {}) => { + const Wrapper = () => { + const { control } = useForm({ defaultValues: { test: false } }); + return ; + }; + + return render(); + }; + + test("renders the checkbox", () => { + setup(); + const checkbox = screen.getByRole("checkbox"); + expect(checkbox).toBeInTheDocument(); + }); + + test("applies input transform function", () => { + const inputTransform = vi.fn().mockReturnValue(true); + setup({ inputTransform }); + const checkbox = screen.getByRole("checkbox"); + expect(checkbox).toBeChecked(); + expect(inputTransform).toHaveBeenCalled(); + }); + + test("calls output transform and onChange callback on change", () => { + const outputTransform = vi.fn().mockReturnValue(true); + const onChangeCallback = vi.fn(); + setup({ outputTransform, onChangeCallback }); + + const checkbox = screen.getByRole("checkbox"); + fireEvent.click(checkbox); + expect(outputTransform).toHaveBeenCalledWith(true, expect.any(Object)); + expect(onChangeCallback).toHaveBeenCalledWith(true); + }); + + test("updates value correctly without transform functions", () => { + setup(); + const checkbox = screen.getByRole("checkbox"); + expect(checkbox).not.toBeChecked(); + fireEvent.click(checkbox); + expect(checkbox).toBeChecked(); + }); + + test("validates input value correctly", () => { + setup(); + const checkbox = screen.getByRole("checkbox"); + expect(checkbox).not.toBeChecked(); + }); +}); diff --git a/ui-next/src/components/v1/react-hook-form/ReactHookFormCheckbox.tsx b/ui-next/src/components/v1/react-hook-form/ReactHookFormCheckbox.tsx new file mode 100644 index 0000000000..16949b40b0 --- /dev/null +++ b/ui-next/src/components/v1/react-hook-form/ReactHookFormCheckbox.tsx @@ -0,0 +1,82 @@ +import { + FieldPath, + FieldValues, + PathValue, + UseControllerProps, + useController, +} from "react-hook-form"; + +import { + ConductorCheckbox, + ConductorCheckboxProps, +} from "../ConductorCheckbox"; + +type ReactHookFormCheckboxProps< + T extends FieldValues, + U extends FieldPath, +> = ConductorCheckboxProps & + UseControllerProps & { + inputTransform?: ( + value: PathValue, + lastFormValues?: T, + ) => ConductorCheckboxProps["value"]; + outputTransform?: (value: boolean, lastFormValues: T) => PathValue; + onChangeCallback?: (value: PathValue) => void; + }; + +const validateInputValue = (value: any) => { + return Boolean(value); +}; + +export default function ReactHookFormCheckbox< + T extends FieldValues, + U extends FieldPath, +>({ + // Controller props + control, + name, + rules, + shouldUnregister, + defaultValue, + + // Manipulate the value before it is passed to the controller + inputTransform = (value) => value, + // Manipulate the value before the onChange is fired + outputTransform, + // Callback to be called when the value changed + onChangeCallback, + + // Checkbox props + ...props +}: ReactHookFormCheckboxProps) { + const { + field: { value, onChange, ...fieldProps }, + } = useController({ + control, + name, + rules, + shouldUnregister, + defaultValue, + }); + + return ( + { + if (outputTransform) { + onChange(outputTransform(newValue, control?._formValues as T)); + onChangeCallback?.( + outputTransform(newValue, control?._formValues as T), + ); + } else { + onChange(newValue); + onChangeCallback?.(newValue as PathValue); + } + }} + /> + ); +} diff --git a/ui-next/src/components/v1/react-hook-form/ReactHookFormDropdown.test.tsx b/ui-next/src/components/v1/react-hook-form/ReactHookFormDropdown.test.tsx new file mode 100644 index 0000000000..be8400fc31 --- /dev/null +++ b/ui-next/src/components/v1/react-hook-form/ReactHookFormDropdown.test.tsx @@ -0,0 +1,105 @@ +import "@testing-library/jest-dom"; +import { fireEvent, render, screen } from "@testing-library/react"; +import { useForm } from "react-hook-form"; +import { Provider as ThemeProvider } from "theme/material/provider"; +import ReactHookFormDropdown from "./ReactHookFormDropdown"; + +describe("ReactHookFormDropdown", () => { + const options = ["Option 1", "Option 2", "Option 3"]; + + const setup = (props: any = {}) => { + const TestComponent = () => { + const { control, watch } = useForm({ + defaultValues: { test: undefined }, + }); + const watchedValues = watch(); + return ( + + +
    {JSON.stringify(watchedValues)}
    +
    + ); + }; + + return render(); + }; + + const getFormValue = () => + JSON.parse(screen.getByTestId("form-value").textContent || ""); + + test("renders the dropdown", () => { + setup(); + const input = screen.getByRole("combobox"); + expect(input).toBeInTheDocument(); + }); + + test("applies input transform function", () => { + const inputTransform = vi.fn().mockReturnValue("Option 3"); + setup({ inputTransform }); + const input = screen.getByRole("combobox"); + expect(input).toHaveValue("Option 3"); + expect(inputTransform).toHaveBeenCalled(); + expect(getFormValue()).toEqual({ test: undefined }); + }); + + test("calls output transform and onChange callback on change", () => { + const outputTransform = vi.fn().mockReturnValue("Option 3"); + const onChangeCallback = vi.fn(); + setup({ outputTransform, onChangeCallback }); + + const input = screen.getByRole("combobox"); + fireEvent.change(input, { target: { value: "Option 1" } }); + + // Open the autocomplete options + fireEvent.focus(input); + fireEvent.keyDown(input, { key: "ArrowDown" }); + + // Select the first option + const option = screen.getByText("Option 1"); + fireEvent.click(option); + + expect(outputTransform).toHaveBeenCalledWith( + "Option 1", + expect.any(Object), + ); + expect(onChangeCallback).toHaveBeenCalledWith("Option 3"); + expect(getFormValue()).toEqual({ test: "Option 3" }); + }); + + test("updates value correctly without transform functions", () => { + setup(); + const input = screen.getByRole("combobox"); + expect(input).toHaveValue(""); + + // Open the autocomplete options + fireEvent.focus(input); + fireEvent.keyDown(input, { key: "ArrowDown" }); + + // Select the second option + const option = screen.getByText("Option 2"); + fireEvent.click(option); + + expect(input).toHaveValue("Option 2"); + expect(getFormValue()).toEqual({ test: "Option 2" }); + }); + + test("validates input value correctly", () => { + setup(); + const input = screen.getByRole("combobox"); + expect(input).toHaveValue(""); + expect(getFormValue()).toEqual({ test: undefined }); + }); + + test("handles multiple and freeSolo correctly", () => { + setup({ multiple: false, freeSolo: true }); + const input = screen.getByRole("combobox"); + fireEvent.change(input, { target: { value: "free solo value" } }); + expect(input).toHaveValue("free solo value"); + expect(getFormValue()).toEqual({ test: "free solo value" }); + }); +}); diff --git a/ui-next/src/components/v1/react-hook-form/ReactHookFormDropdown.tsx b/ui-next/src/components/v1/react-hook-form/ReactHookFormDropdown.tsx new file mode 100644 index 0000000000..8412c107ab --- /dev/null +++ b/ui-next/src/components/v1/react-hook-form/ReactHookFormDropdown.tsx @@ -0,0 +1,89 @@ +import { + FieldPath, + FieldValues, + PathValue, + UseControllerProps, + useController, +} from "react-hook-form"; +import _isNil from "lodash/isNil"; + +import { + ConductorAutoComplete, + ConductorAutocompleteProps, +} from "components/v1/ConductorAutoComplete"; + +type ReactHookFormDropdownProps< + T extends FieldValues, + U extends FieldPath, + V, +> = ConductorAutocompleteProps & + UseControllerProps & { + inputTransform?: (value: PathValue, lastFormValues?: T) => V | V[]; + outputTransform?: (value: V | V[], lastFormValues: T) => PathValue; + onChangeCallback?: (value: PathValue) => void; + }; + +const validateInputValue = (value: any) => { + if (_isNil(value)) { + return null; + } + return value; +}; + +export default function ReactHookFormDropdown< + T extends FieldValues, + U extends FieldPath, + V = string, +>({ + // Controller props + control, + name, + rules, + shouldUnregister, + defaultValue, + + // Manipulate the value before it is passed to the controller + inputTransform = (value) => value, + // Manipulate the value before the onChange is fired + outputTransform, + // Callback to be called when the value changed + onChangeCallback, + + // Checkbox props + ...props +}: ReactHookFormDropdownProps) { + const { + field: { value, onChange, ...fieldProps }, + } = useController({ + control, + name, + rules, + shouldUnregister, + defaultValue, + }); + return ( + { + if (outputTransform) { + onChange(outputTransform(newValue, control?._formValues as T)); + onChangeCallback?.( + outputTransform(newValue, control?._formValues as T), + ); + } else { + onChange(newValue); + onChangeCallback?.(newValue as PathValue); + } + }} + onInputChange={(_, newValue: string) => { + if (!props.multiple && props.freeSolo) { + onChange(newValue); + } + }} + /> + ); +} diff --git a/ui-next/src/components/v1/react-hook-form/ReactHookFormEditor.tsx b/ui-next/src/components/v1/react-hook-form/ReactHookFormEditor.tsx new file mode 100644 index 0000000000..bc866097b0 --- /dev/null +++ b/ui-next/src/components/v1/react-hook-form/ReactHookFormEditor.tsx @@ -0,0 +1,43 @@ +import { + FieldValues, + UseControllerProps, + useController, +} from "react-hook-form"; +import { Editor, EditorProps } from "@monaco-editor/react"; + +type ReactHookFormEditorProps = EditorProps & + UseControllerProps; + +export default function ReactHookFormEditor({ + // Controller props + control, + name, + rules, + shouldUnregister, + defaultValue, + + // Editor props + ...props +}: ReactHookFormEditorProps) { + const { + field: { onChange, ...fieldProps }, + } = useController({ + control, + name, + rules, + shouldUnregister, + defaultValue, + }); + return ( + { + if (typeof value === "string") { + onChange(value, event); + props?.onChange?.(value, event); + } + }} + /> + ); +} diff --git a/ui-next/src/components/v1/react-hook-form/ReactHookFormFlatMapForm.tsx b/ui-next/src/components/v1/react-hook-form/ReactHookFormFlatMapForm.tsx new file mode 100644 index 0000000000..0e7781a5dd --- /dev/null +++ b/ui-next/src/components/v1/react-hook-form/ReactHookFormFlatMapForm.tsx @@ -0,0 +1,88 @@ +import { + FieldPath, + FieldValues, + PathValue, + UseControllerProps, + useController, +} from "react-hook-form"; +import _isNil from "lodash/isNil"; + +import { + ConductorFlatMapFormBase, + ConductorFlatMapFormProps, +} from "../FlatMapForm/ConductorFlatMapForm"; + +type ReactHookFormFlatMapFormProps< + T extends FieldValues, + U extends FieldPath, +> = ConductorFlatMapFormProps & + UseControllerProps & { + inputTransform?: ( + value: PathValue, + lastFormValues?: T, + ) => ConductorFlatMapFormProps["value"]; + outputTransform?: ( + value: ConductorFlatMapFormProps["value"], + lastFormValues: T, + ) => PathValue; + onChangeCallback?: (value: PathValue) => void; + }; + +const validateInputValue = (value: any) => { + if (_isNil(value)) { + return {}; + } + return value; +}; + +export default function ReactHookFormFlatMapForm< + T extends FieldValues, + U extends FieldPath, +>({ + // Controller props + control, + name, + rules, + shouldUnregister, + defaultValue, + + // Manipulate the value before it is passed to the controller + inputTransform = (value) => value, + // Manipulate the value before the onChange is fired + outputTransform, + // Callback to be called when the value changed + onChangeCallback, + + // Checkbox props + ...props +}: ReactHookFormFlatMapFormProps) { + const { + field: { value, onChange, ...fieldProps }, + } = useController({ + control, + name, + rules, + shouldUnregister, + defaultValue, + }); + return ( + { + if (outputTransform) { + onChange(outputTransform(newValue, control?._formValues as T)); + onChangeCallback?.( + outputTransform(newValue, control?._formValues as T), + ); + } else { + onChange(newValue); + onChangeCallback?.(newValue as PathValue); + } + }} + /> + ); +} diff --git a/ui-next/src/components/v1/react-hook-form/ReactHookFormIdempotencyForm.test.tsx b/ui-next/src/components/v1/react-hook-form/ReactHookFormIdempotencyForm.test.tsx new file mode 100644 index 0000000000..9163bf4385 --- /dev/null +++ b/ui-next/src/components/v1/react-hook-form/ReactHookFormIdempotencyForm.test.tsx @@ -0,0 +1,46 @@ +import { render, screen } from "@testing-library/react"; +import { useForm } from "react-hook-form"; +import { Provider as ThemeProvider } from "theme/material/provider"; +import "@testing-library/jest-dom"; +import ReactHookFormIdempotencyForm from "./ReactHookFormIdempotencyForm"; + +describe("ReactHookFormIdempotencyForm", () => { + const setup = (props: any = {}) => { + const TestComponent = () => { + const { control, watch } = useForm({ + defaultValues: { idempotencyKey: "", idempotencyStrategy: undefined }, + }); + const watchedValues = watch(); + return ( + + +
    {JSON.stringify(watchedValues)}
    +
    + ); + }; + + return render(); + }; + + const getFormValue = () => + JSON.parse(screen.getByTestId("form-value").textContent || "{}"); + + test("renders the form correctly", () => { + setup(); + expect(screen.getByLabelText("Idempotency key")).toBeInTheDocument(); + }); + + test("validates input value correctly", () => { + setup(); + const input = screen.getByLabelText("Idempotency key"); + expect(input).toHaveValue(""); + expect(getFormValue()).toEqual({ + idempotencyKey: "", + idempotencyStrategy: undefined, + }); + }); +}); diff --git a/ui-next/src/components/v1/react-hook-form/ReactHookFormIdempotencyForm.tsx b/ui-next/src/components/v1/react-hook-form/ReactHookFormIdempotencyForm.tsx new file mode 100644 index 0000000000..f5b2c2cc46 --- /dev/null +++ b/ui-next/src/components/v1/react-hook-form/ReactHookFormIdempotencyForm.tsx @@ -0,0 +1,87 @@ +import { + FieldPath, + FieldValues, + PathValue, + UseControllerProps, + useController, +} from "react-hook-form"; +import _isNil from "lodash/isNil"; + +import IdempotencyForm, { + IdempotencyFormProps, +} from "pages/runWorkflow/IdempotencyForm"; + +type ReactHookFormIdempotencyFormProps< + T extends FieldValues, + U extends FieldPath, +> = Omit & + UseControllerProps & { + inputTransform?: ( + value: PathValue, + lastFormValues?: T, + ) => IdempotencyFormProps["idempotencyValues"]; + outputTransform?: ( + value: IdempotencyFormProps["idempotencyValues"], + lastFormValues: T, + ) => PathValue; + onChangeCallback?: (value: PathValue) => void; + }; + +const validateInputValue = (value: any) => { + if (_isNil(value)) { + return {}; + } + return value; +}; + +export default function ReactHookFormIdempotencyForm< + T extends FieldValues, + U extends FieldPath, +>({ + // Controller props + control, + name, + rules, + shouldUnregister, + defaultValue, + + // Manipulate the value before it is passed to the controller + inputTransform = (value) => value, + // Manipulate the value before the onChange is fired + outputTransform, + // Callback to be called when the value changed + onChangeCallback, + + // Checkbox props + ...props +}: ReactHookFormIdempotencyFormProps) { + const { + field: { value, onChange, ...fieldProps }, + } = useController({ + control, + name, + rules, + shouldUnregister, + defaultValue, + }); + return ( + { + if (outputTransform) { + onChange(outputTransform(values, control?._formValues as T)); + onChangeCallback?.( + outputTransform(values, control?._formValues as T), + ); + } else { + onChange(values); + onChangeCallback?.(values as PathValue); + } + }} + /> + ); +} diff --git a/ui-next/src/components/v1/react-hook-form/ReactHookFormInput.test.tsx b/ui-next/src/components/v1/react-hook-form/ReactHookFormInput.test.tsx new file mode 100644 index 0000000000..d9c0bcb3ed --- /dev/null +++ b/ui-next/src/components/v1/react-hook-form/ReactHookFormInput.test.tsx @@ -0,0 +1,73 @@ +import "@testing-library/jest-dom"; +import { fireEvent, render, screen } from "@testing-library/react"; +import { useForm } from "react-hook-form"; +import { Provider as ThemeProvider } from "theme/material/provider"; +import ReactHookFormInput from "./ReactHookFormInput"; + +describe("ReactHookFormInput", () => { + const setup = (props: any = {}) => { + const TestComponent = () => { + const { control, watch } = useForm({ defaultValues: { test: "" } }); + const watchedValues = watch(); + + return ( + + +
    {JSON.stringify(watchedValues)}
    +
    + ); + }; + + return render(); + }; + + const getFormValue = () => + JSON.parse(screen.getByTestId("form-value").textContent || ""); + + test("renders the input", () => { + setup(); + const input = screen.getByRole("textbox"); + expect(input).toBeInTheDocument(); + }); + + test("applies input transform function", () => { + const inputTransform = vi.fn().mockReturnValue("transformed value"); + setup({ inputTransform }); + const input = screen.getByRole("textbox"); + expect(input).toHaveValue("transformed value"); + expect(inputTransform).toHaveBeenCalled(); + expect(getFormValue()).toEqual({ test: "" }); + }); + + test("calls output transform and onChange callback on change", () => { + const outputTransform = vi.fn().mockReturnValue("transformed output"); + const onChangeCallback = vi.fn(); + setup({ outputTransform, onChangeCallback }); + + const input = screen.getByRole("textbox"); + fireEvent.change(input, { target: { value: "new value" } }); + + expect(outputTransform).toHaveBeenCalledWith( + "new value", + expect.any(Object), + ); + expect(onChangeCallback).toHaveBeenCalledWith("transformed output"); + expect(getFormValue()).toEqual({ test: "transformed output" }); + }); + + test("updates value correctly without transform functions", () => { + setup(); + const input = screen.getByRole("textbox"); + expect(input).toHaveValue(""); + fireEvent.change(input, { target: { value: "new value" } }); + expect(input).toHaveValue("new value"); + expect(getFormValue()).toEqual({ test: "new value" }); + }); + + test("validates input value correctly", () => { + setup(); + const input = screen.getByRole("textbox"); + expect(input).toHaveValue(""); + expect(getFormValue()).toEqual({ test: "" }); + }); +}); diff --git a/ui-next/src/components/v1/react-hook-form/ReactHookFormInput.tsx b/ui-next/src/components/v1/react-hook-form/ReactHookFormInput.tsx new file mode 100644 index 0000000000..5e9de9f8ed --- /dev/null +++ b/ui-next/src/components/v1/react-hook-form/ReactHookFormInput.tsx @@ -0,0 +1,79 @@ +import _isNil from "lodash/isNil"; +import { + FieldPath, + FieldValues, + PathValue, + UseControllerProps, + useController, +} from "react-hook-form"; + +import ConductorInput, { ConductorInputProps } from "../ConductorInput"; + +type ReactHookFormInputProps< + T extends FieldValues, + U extends FieldPath, +> = ConductorInputProps & + UseControllerProps & { + inputTransform?: (value: PathValue, lastFormValues?: T) => string; + outputTransform?: (value: string, lastFormValues?: T) => PathValue; + onChangeCallback?: (value: PathValue) => void; + }; + +const validateInputValue = (value: any) => { + if (_isNil(value)) { + return ""; + } + return value; +}; + +export default function ReactHookFormInput< + T extends FieldValues, + U extends FieldPath, +>({ + // Controller props + control, + name, + rules, + shouldUnregister, + defaultValue, + + // Manipulate the value before it is passed to the controller + inputTransform = (value) => value, + // Manipulate the value before the onChange is fired + outputTransform, + // Callback to be called when the value changed + onChangeCallback, + + // Checkbox props + ...props +}: ReactHookFormInputProps) { + const { + field: { value, onChange, ...fieldProps }, + } = useController({ + control, + name, + rules, + shouldUnregister, + defaultValue, + }); + return ( + { + if (outputTransform) { + onChange( + outputTransform(event.target.value, control?._formValues as T), + ); + onChangeCallback?.(outputTransform(event.target.value)); + } else { + onChange(event.target.value); + onChangeCallback?.(event.target.value as PathValue); + } + }} + /> + ); +} diff --git a/ui-next/src/components/v1/react-hook-form/ReactHookFormNameVersionField.test.tsx b/ui-next/src/components/v1/react-hook-form/ReactHookFormNameVersionField.test.tsx new file mode 100644 index 0000000000..a8989f74f2 --- /dev/null +++ b/ui-next/src/components/v1/react-hook-form/ReactHookFormNameVersionField.test.tsx @@ -0,0 +1,108 @@ +import "@testing-library/jest-dom"; +import { fireEvent, render, screen } from "@testing-library/react"; +import { useForm } from "react-hook-form"; +import { Provider as ThemeProvider } from "theme/material/provider"; +import { useFetch } from "utils/query"; +import ReactHookFormNameVersionField from "./ReactHookFormNameVersionField"; + +// Mocking the data fetching hook +vi.mock("utils/query", () => ({ + useFetch: vi.fn(), +})); + +describe("ReactHookFormNameVersionField", () => { + const setup = (props = {}) => { + const TestComponent = () => { + (useFetch as ReturnType).mockReturnValue({ + data: [ + { name: "option0", versions: [1, 2] }, + { name: "option1", versions: [1, 2, 3] }, + { name: "option2", versions: [1, 2] }, + ], + }); + const { control, watch } = useForm({ + defaultValues: { test: { name: "", version: "" } }, + }); + const watchedValues = watch(); + + return ( + + +
    {JSON.stringify(watchedValues)}
    +
    + ); + }; + + return render(); + }; + + const getFormValue = () => + JSON.parse(screen.getByTestId("form-value").textContent || ""); + + test("renders the autocomplete and select inputs", () => { + setup(); + expect(document.getElementById("name-field")).toBeInTheDocument(); + expect(document.getElementById("version-field")).toBeInTheDocument(); + }); + + test("applies input transform function", () => { + const inputTransform = vi + .fn() + .mockReturnValue({ name: "option1", version: 1 }); + setup({ inputTransform }); + + expect(document.getElementById("name-field")).toHaveValue("option1"); + expect(document.getElementById("version-field")).toHaveTextContent( + "Version 1", + ); + expect(inputTransform).toHaveBeenCalled(); + expect(getFormValue()).toEqual({ test: { name: "", version: "" } }); + }); + + test("calls output transform and onChange callback on change", () => { + const outputTransform = vi + .fn() + .mockReturnValue({ name: "option0", version: 1 }); + const onChangeCallback = vi.fn(); + setup({ outputTransform, onChangeCallback }); + + const nameField = document.getElementById("name-field"); + const versionField = document.getElementById("version-field"); + + if (!nameField || !versionField) { + throw new Error("Name field or Version field not found"); + } + + // Open the autocomplete options + fireEvent.focus(nameField); + fireEvent.keyDown(nameField, { key: "ArrowDown" }); + + // Select the second option + const option = screen.getByText("option1"); + fireEvent.click(option); + + expect(outputTransform).toHaveBeenCalledWith( + { name: "option1", version: undefined }, + expect.any(Object), + ); + expect(onChangeCallback).toHaveBeenCalledWith({ + name: "option0", + version: 1, + }); + expect(getFormValue()).toEqual({ + test: { name: "option0", version: 1 }, + }); + }); +}); diff --git a/ui-next/src/components/v1/react-hook-form/ReactHookFormNameVersionField.tsx b/ui-next/src/components/v1/react-hook-form/ReactHookFormNameVersionField.tsx new file mode 100644 index 0000000000..38a0d5af98 --- /dev/null +++ b/ui-next/src/components/v1/react-hook-form/ReactHookFormNameVersionField.tsx @@ -0,0 +1,91 @@ +import { + FieldPath, + FieldValues, + PathValue, + useController, + UseControllerProps, +} from "react-hook-form"; +import _isNil from "lodash/isNil"; + +import { + ConductorNameVersionField, + ConductorNameVersionFieldProps, +} from "components/v1/ConductorNameVersionField"; + +type ReactHookFormNameVersionFieldProps< + T extends FieldValues, + U extends FieldPath, +> = ConductorNameVersionFieldProps & + UseControllerProps & { + inputTransform?: ( + value: PathValue, + lastFormValues?: T, + ) => ConductorNameVersionFieldProps["value"]; + outputTransform?: ( + value: + | { + name?: string; + version?: number; + } + | undefined, + lastFormValues: T, + ) => PathValue; + onChangeCallback?: (value: PathValue) => void; + }; + +const validateInputValue = (value: any) => { + if (_isNil(value)) { + return {}; + } + return value; +}; + +export default function ReactHookFormNameVersionField< + T extends FieldValues, + U extends FieldPath, +>({ + // Controller props + control, + name, + rules, + shouldUnregister, + defaultValue, + + // Manipulate the value before it is passed to the controller + inputTransform = (value) => value, + // Manipulate the value before the onChange is fired + outputTransform, + // Callback to be called when the value changed + onChangeCallback, + + // Dropdown props + ...props +}: ReactHookFormNameVersionFieldProps) { + const { + field: { value, onChange, ...fieldProps }, + } = useController({ + control, + name, + rules, + shouldUnregister, + defaultValue, + }); + return ( + { + if (outputTransform) { + onChange(outputTransform(value, control?._formValues as T)); + onChangeCallback?.(outputTransform(value, control?._formValues as T)); + } else { + onChange(value); + onChangeCallback?.(value as PathValue); + } + }} + /> + ); +} diff --git a/ui-next/src/components/v1/theme/index.ts b/ui-next/src/components/v1/theme/index.ts new file mode 100644 index 0000000000..2832fe7066 --- /dev/null +++ b/ui-next/src/components/v1/theme/index.ts @@ -0,0 +1,2 @@ +export { ColorModeProvider as ThemeProvider } from "./material/provider"; +export { default as getTheme } from "./theme"; diff --git a/ui-next/src/components/v1/theme/material/components/buttons.ts b/ui-next/src/components/v1/theme/material/components/buttons.ts new file mode 100644 index 0000000000..05a377eb2a --- /dev/null +++ b/ui-next/src/components/v1/theme/material/components/buttons.ts @@ -0,0 +1,283 @@ +import { PaletteMode, Theme } from "@mui/material"; +import { Components } from "@mui/material/styles"; +import { colors } from "theme/tokens/variables"; + +const lightButton: Partial> = { + MuiButton: { + defaultProps: { + disableRipple: true, + variant: "contained", + color: "primary", + size: "medium", + }, + styleOverrides: { + root: { + textTransform: "none", + borderRadius: "6px", + transition: "none", + fontWeight: 500, + boxShadow: "none", + padding: "8px 12px 8px 12px", + }, + sizeSmall: { + minHeight: "28px", + height: "28px", + fontSize: "12px", + }, + sizeMedium: { + minHeight: "36px", + height: "36px", + fontSize: "14px", + fontWeight: 500, + }, + sizeLarge: { + minHeight: "50px", + height: "50px", + fontSize: "16px", + }, + contained: { + border: `1px solid ${colors.sidebarFaintGrey}`, + }, + containedPrimary: { + color: colors.white, + backgroundColor: colors.blueLightMode, + border: `1px solid ${colors.blueLightMode}`, + + ":hover": { + color: colors.white, + backgroundColor: colors.blueLightMode, + border: `1px solid ${colors.blueLightMode}`, + boxShadow: `3px 3px 0px 0px ${colors.primaryHoverBoxShadow}`, + transition: "0.3s ease-in-out", + }, + + ":active": { + color: colors.sidebarBlacky, + backgroundColor: colors.darkBlueLightMode, + borderColor: colors.darkBlueLightMode, + boxShadow: "none", + transition: "0.3s ease-in-out", + }, + + "&.Mui-disabled": { + border: "none", + color: colors.sidebarFaintGrey, + backgroundColor: colors.sidebarBarelyPastWhite, + }, + }, + outlinedPrimary: { + color: colors.sidebarBlacky, + backgroundColor: undefined, + border: `1px solid ${colors.blueLight}`, + }, + textPrimary: { + color: colors.blueLightMode, + }, + containedSecondary: { + color: colors.blueLightMode, + backgroundColor: colors.white, + border: `1px solid ${colors.blueLightMode}`, + + ":hover": { + color: colors.blueLightMode, + backgroundColor: colors.white, + border: `1px solid ${colors.blueLightMode}`, + boxShadow: `3px 3px 0px 0px ${colors.secondaryHoverBoxShadow}`, + transition: "0.3s ease-in-out", + }, + + ":active": { + color: colors.sidebarBlacky, + backgroundColor: colors.darkBlueLightMode, + borderColor: colors.darkBlueLightMode, + boxShadow: "none", + transition: "0.3s ease-in-out", + }, + + "&.Mui-disabled": { + color: colors.sidebarFaintGrey, + backgroundColor: colors.sidebarBarelyPastWhite, + borderColor: colors.sidebarFaintGrey, + opacity: 0.8, + }, + }, + outlinedSecondary: { + color: colors.sidebarGreyDark, + borderColor: colors.sidebarGreyDark, + }, + textSecondary: { + color: colors.sidebarGreyDark, + }, + // @ts-ignore + containedTertiary: { + color: colors.sidebarGrey, + backgroundColor: colors.white, + border: `1px solid ${colors.sidebarFaintGrey}`, + + ":hover": { + color: colors.greyBg, + backgroundColor: colors.white, + borderColor: colors.sidebarFaintGrey, + boxShadow: `3px 3px 0px 0px ${colors.tertiaryHoverBoxShadow}`, + transition: "0.3s ease-in-out", + }, + + ":active": { + color: colors.sidebarGreyDark, + backgroundColor: colors.darkBlueLightMode, + borderColor: colors.darkBlueLightMode, + boxShadow: "none", + transition: "0.3s ease-in-out", + }, + + "&.Mui-disabled": { + color: colors.sidebarFaintGrey, + backgroundColor: colors.sidebarBarelyPastWhite, + borderColor: colors.sidebarFaintGrey, + }, + }, + outlinedTertiary: { + color: colors.sidebarGrey, + border: `1px solid ${colors.sidebarGrey}`, + + ":hover": { + color: colors.greyBg, + borderColor: colors.sidebarFaintGrey, + }, + }, + textTertiary: { + color: colors.sidebarGrey, + }, + }, + }, + MuiIconButton: { + styleOverrides: { + root: { + color: colors.gray04, + borderColor: colors.gray04, + "&.Mui-disabled": { + color: colors.gray08, + borderColor: colors.gray08, + }, + "&:hover": { + backgroundColor: undefined, + }, + }, + }, + }, +}; + +const darkButton: Partial> = { + MuiButton: { + defaultProps: { + disableRipple: true, + variant: "contained", + color: "primary", + size: "medium", + }, + styleOverrides: { + root: { + textTransform: "none", + borderRadius: "6px", + transition: "none", + fontWeight: 500, + boxShadow: "none", + padding: "8px", + + "&.Mui-disabled": { + border: "none", + color: colors.sidebarFaintGrey, + backgroundColor: colors.sidebarBarelyPastWhite, + }, + + ":after": { + content: '""', + position: "absolute", + zIndex: -1, + right: 0, + bottom: 0, + width: "100%", + height: "100%", + background: `${colors.blueBackground}`, + border: `1px solid ${colors.sidebarGreyDark}`, + borderRadius: "6px", + opacity: 0, + transition: "opacity 0.3s ease-in-out, transform 0.3s ease-in-out", + }, + + ":hover": { + color: colors.sidebarFaintGrey, + backgroundColor: colors.sidebarGreyDark, + border: `1px solid ${colors.sidebarGreyDark}`, + + ":after": { + opacity: 1, + right: -5, + bottom: -5, + }, + }, + }, + sizeSmall: { + minHeight: "28px", + height: "28px", + fontSize: "10pt", + }, + sizeMedium: { + minHeight: "36px", + height: "36px", + fontSize: "11pt", + fontWeight: 500, + }, + sizeLarge: { + minHeight: "50px", + height: "50px", + fontSize: "14pt", + }, + outlinedPrimary: {}, + outlinedSecondary: { + color: colors.gray12, + borderColor: colors.gray12, + "&.Mui-disabled": { + color: colors.gray05, + borderColor: colors.gray05, + }, + }, + contained: { + border: `1px solid ${colors.sidebarFaintGrey}`, + }, + containedPrimary: { + "&.Mui-disabled": { + color: colors.gray09, + backgroundColor: colors.gray05, + }, + }, + containedSecondary: { + "&.Mui-disabled": { + color: colors.gray05, + backgroundColor: colors.gray02, + }, + }, + }, + }, + MuiIconButton: { + styleOverrides: { + root: { + color: colors.gray12, + borderColor: colors.gray12, + "&.Mui-disabled": { + color: colors.gray05, + borderColor: colors.gray05, + }, + "&:hover": { + backgroundColor: colors.gray06, + }, + }, + }, + }, +}; + +const buttons = (mode: PaletteMode): Components => { + return mode === "dark" ? darkButton : lightButton; +}; + +export default buttons; diff --git a/ui-next/src/components/v1/theme/material/components/buttonsGroup.ts b/ui-next/src/components/v1/theme/material/components/buttonsGroup.ts new file mode 100644 index 0000000000..8940c3df25 --- /dev/null +++ b/ui-next/src/components/v1/theme/material/components/buttonsGroup.ts @@ -0,0 +1,248 @@ +import { PaletteMode, Theme } from "@mui/material"; +import { Components } from "@mui/material/styles"; +import { colors } from "theme/tokens/variables"; + +const lightButtonGroup: Partial> = { + MuiButtonGroup: { + defaultProps: { + disableRipple: true, + variant: "contained", + color: "primary", + size: "medium", + }, + styleOverrides: { + root: { + textTransform: "none", + borderRadius: "6px", + transition: "none", + fontWeight: 500, + boxShadow: "none", + padding: "8px 12px 8px 12px", + "&.Mui-disabled": { + color: colors.sidebarFaintGrey, + backgroundColor: colors.sidebarBarelyPastWhite, + border: `1px solid ${colors.defaultModalBackdropColor}`, + }, + }, + + groupedContainedPrimary: { + color: colors.white, + backgroundColor: colors.blueLightMode, + border: `1px solid ${colors.blueLightMode}`, + + ":not(:last-of-type)": { + border: `none`, + borderRight: "1px solid white", + }, + + ":hover": { + color: colors.white, + backgroundColor: colors.blueLightMode, + border: `1px solid ${colors.blueLightMode}`, + boxShadow: `3px 3px 0px 0px ${colors.primaryHoverBoxShadow}`, + ":not(:last-of-type)": { + border: `none`, + borderRight: "1px solid white", + }, + }, + + ":active": { + color: colors.sidebarBlacky, + backgroundColor: colors.darkBlueLightMode, + borderColor: colors.darkBlueLightMode, + boxShadow: "none", + }, + + "&.Mui-disabled": { + color: colors.sidebarFaintGrey, + backgroundColor: colors.sidebarBarelyPastWhite, + border: `1px solid ${colors.defaultModalBackdropColor}`, + ":not(:last-of-type)": { + border: `1px solid ${colors.defaultModalBackdropColor}`, + }, + }, + }, + + groupedContainedSecondary: { + color: colors.blueLightMode, + backgroundColor: colors.white, + border: `1px solid ${colors.blueLightMode}`, + + ":hover": { + color: colors.blueLightMode, + backgroundColor: colors.white, + border: `1px solid ${colors.blueLightMode}`, + boxShadow: `3px 3px 0px 0px ${colors.secondaryHoverBoxShadow}`, + }, + + ":active": { + color: colors.sidebarBlacky, + backgroundColor: colors.darkBlueLightMode, + borderColor: colors.darkBlueLightMode, + boxShadow: "none", + }, + + ":not(:last-of-type)": { + border: `1px solid ${colors.blueLightMode}`, + }, + "&.Mui-disabled": { + color: colors.sidebarFaintGrey, + backgroundColor: colors.sidebarBarelyPastWhite, + border: `1px solid ${colors.defaultModalBackdropColor}`, + ":not(:last-of-type)": { + border: `1px solid ${colors.defaultModalBackdropColor}`, + }, + }, + }, + // @ts-ignore + groupedContainedTertiary: { + color: colors.sidebarGrey, + backgroundColor: colors.white, + border: `1px solid ${colors.sidebarFaintGrey}`, + + ":not(:last-of-type)": { + border: `1px solid ${colors.sidebarFaintGrey}`, + }, + ":hover": { + color: colors.greyBg, + backgroundColor: colors.white, + borderColor: colors.sidebarFaintGrey, + boxShadow: `3px 3px 0px 0px ${colors.tertiaryHoverBoxShadow}`, + }, + + ":active": { + color: colors.sidebarGreyDark, + backgroundColor: colors.darkBlueLightMode, + borderColor: colors.darkBlueLightMode, + boxShadow: "none", + }, + + "&.Mui-disabled": { + color: colors.sidebarFaintGrey, + backgroundColor: colors.sidebarBarelyPastWhite, + borderColor: colors.defaultModalBackdropColor, + ":not(:last-of-type)": { + border: `1px solid ${colors.defaultModalBackdropColor}`, + }, + }, + }, + }, + }, + MuiIconButton: { + styleOverrides: { + root: { + color: colors.gray04, + borderColor: colors.gray04, + "&.Mui-disabled": { + color: colors.gray08, + borderColor: colors.gray08, + }, + "&:hover": { + backgroundColor: undefined, + }, + }, + }, + }, +}; + +const darkButtonGroup: Partial> = { + MuiButtonGroup: { + defaultProps: { + disableRipple: true, + variant: "contained", + color: "primary", + size: "medium", + }, + styleOverrides: { + root: { + textTransform: "none", + borderRadius: "6px", + transition: "none", + fontWeight: 500, + boxShadow: "none", + padding: "8px", + + "&.Mui-disabled": { + color: colors.sidebarFaintGrey, + backgroundColor: colors.sidebarBarelyPastWhite, + }, + }, + groupedContained: { + ":after": { + content: '""', + position: "absolute", + zIndex: -1, + right: 0, + bottom: 0, + width: "100%", + height: "100%", + background: `${colors.blueBackground}`, + border: `1px solid ${colors.sidebarGreyDark}`, + borderRadius: "6px", + opacity: 0, + transition: "opacity 0.3s ease-in-out, transform 0.3s ease-in-out", + }, + + ":hover": { + color: colors.sidebarFaintGrey, + backgroundColor: colors.sidebarGreyDark, + border: `1px solid ${colors.sidebarGreyDark}`, + + ":after": { + opacity: 1, + right: -5, + bottom: -5, + }, + }, + }, + + groupedContainedPrimary: { + "&.Mui-disabled": { + color: colors.gray09, + backgroundColor: colors.gray05, + }, + }, + + groupedContainedSecondary: { + "&.Mui-disabled": { + color: colors.gray05, + backgroundColor: colors.gray02, + }, + ":not(:last-of-type)": { + border: `1px solid ${colors.blueLightMode}`, + }, + }, + // @ts-ignore + groupedContainedTertiary: { + "&.Mui-disabled": { + color: colors.gray05, + backgroundColor: colors.gray02, + }, + ":not(:last-of-type)": { + border: `1px solid ${colors.sidebarFaintGrey}`, + }, + }, + }, + }, + MuiIconButton: { + styleOverrides: { + root: { + color: colors.gray12, + borderColor: colors.gray12, + "&.Mui-disabled": { + color: colors.gray05, + borderColor: colors.gray05, + }, + "&:hover": { + backgroundColor: colors.gray06, + }, + }, + }, + }, +}; + +const buttonsGroup = (mode: PaletteMode): Components => { + return mode === "dark" ? darkButtonGroup : lightButtonGroup; +}; + +export default buttonsGroup; diff --git a/ui-next/src/components/v1/theme/material/components/formControls.ts b/ui-next/src/components/v1/theme/material/components/formControls.ts new file mode 100644 index 0000000000..b7ac504b15 --- /dev/null +++ b/ui-next/src/components/v1/theme/material/components/formControls.ts @@ -0,0 +1,268 @@ +import { PaletteMode, Theme } from "@mui/material"; +import { Components } from "@mui/material/styles"; + +import { colors, fontSizes, fontWeights } from "theme/tokens/variables"; +import baseTheme from "theme/material/baseTheme"; + +// TODO: get rid of these components after applying new inputs whole app +const enabledNewInputs = true; + +export const SMALL_INPUT_HEIGHT = "36px"; + +export const inputLabelIdleStyles = enabledNewInputs + ? {} + : { + transform: "none", + position: "relative", + fontWeight: fontWeights.fontWeight1, + fontSize: fontSizes.fontSize2, + paddingLeft: 0, + marginBottom: ".3em", + marginTop: 0, + color: colors.gray07, + }; + +export const inputLabelFocusedStyles = { + color: colors.black, +}; + +const formControls = (mode: PaletteMode): Components => { + const darkMode = mode === "dark"; + + return { + MuiFormControl: { + defaultProps: { + size: "small", + }, + styleOverrides: { + root: { + display: "block", + }, + }, + }, + MuiInputBase: { + styleOverrides: { + root: { + fontSize: fontSizes.fontSize2, + }, + input: { + "&[type=number]::-webkit-inner-spin-button ": { + appearance: "none", + margin: 0, + }, + }, + sizeSmall: { + minHeight: SMALL_INPUT_HEIGHT, + }, + }, + }, + MuiTextField: { + defaultProps: { + variant: "outlined", + InputProps: { + notched: false, + }, + InputLabelProps: { + shrink: true, + }, + }, + }, + MuiCheckbox: { + defaultProps: { + size: "small", + }, + styleOverrides: { + root: ({ theme }) => ({ + fontSize: fontSizes.fontSize0, + padding: theme.spacing(2), + }), + colorSecondary: ({ theme }) => ({ + color: colors.blackLight, + "&$checked": { + color: theme.palette.primary.main, + }, + "&$disabled": { + color: colors.blackXLight, + }, + }), + }, + }, + MuiSwitch: { + styleOverrides: { + root: { + padding: 0, + marginRight: 8, + marginLeft: 8, + height: 20, + width: 40, + "&:hover": { + "& > $track": { + backgroundColor: colors.gray05, + }, + "& > $checked + $track": { + backgroundColor: colors.brand05, + }, + }, + }, + thumb: { + borderRadius: 8, + width: 16, + height: 16, + color: "white", + boxShadow: + "0px 1px 2px 0px rgba(0, 0, 0, 0.4), 0px 0px 1px 0px rgba(0, 0, 0, 0.4)", + }, + track: ({ theme }) => ({ + backgroundColor: colors.gray07, + borderRadius: 10, + opacity: 1, + ".Mui-checked.Mui-checked + &": { + // track - checked + backgroundColor: theme.palette.green.primary, + opacity: 1, + }, + }), + switchBase: { + padding: 2, + "&$checked": { + // transform: "translateX(100%)", + "& + $track": { + opacity: 1, + }, + }, + }, + colorPrimary: ({ theme }) => ({ + "&$checked": { + color: theme.palette.common.white, + }, + "&$checked + $track": { + backgroundColor: theme.palette.primary.main, + }, + }), + }, + }, + MuiRadio: { + styleOverrides: { + root: ({ theme }) => ({ + padding: theme.spacing(2), + }), + }, + }, + MuiOutlinedInput: enabledNewInputs + ? {} + : { + defaultProps: { + notched: false, + }, + styleOverrides: { + notchedOutline: { + top: 0, + "& legend": { + // force-disable notched legends + display: "none", + }, + }, + root: { + borderColor: mode === "light" ? colors.gray12 : colors.gray06, + top: 0, + backgroundColor: mode === "light" ? "white" : "none", + "&:hover .MuiOutlinedInput-notchedOutline": { + borderColor: mode === "light" ? colors.gray09 : colors.gray09, + }, + }, + input: ({ theme }) => ({ + paddingLeft: theme.spacing(3), + paddingRight: theme.spacing(3), + marginBottom: "-2px", + }), + }, + }, + MuiFormControlLabel: { + styleOverrides: { + root: { + marginLeft: -8, + }, + }, + }, + MuiInputLabel: { + defaultProps: { + shrink: true, + }, + styleOverrides: { + root: { + pointerEvents: enabledNewInputs ? "auto" : "none", + color: baseTheme.palette.text.primary, + "&.MuiInputLabel-outlined": { + "&.MuiInputLabel-shrink": inputLabelIdleStyles, + "&.MuiInputLabel-focused": inputLabelFocusedStyles, + }, + }, + }, + }, + MuiFormHelperText: { + styleOverrides: { + contained: ({ theme }) => ({ + margin: 0, + marginTop: theme.spacing(2), + }), + }, + }, + MuiSelect: { + styleOverrides: { + icon: { + fontSize: fontSizes.fontSize5, + color: + mode === "dark" ? colors.gray12 : baseTheme.palette.text.primary, + }, + }, + }, + // MuiPickersClockNumber: { + // defaultProps: { + // clockNumber: { + // top: 6, + // }, + // }, + // }, + MuiAutocomplete: { + defaultProps: { + componentsProps: { + paper: { + elevation: 3, + }, + }, + }, + styleOverrides: { + paper: { + fontSize: fontSizes.fontSize2, + boxShadow: `0 0 10px ${ + darkMode ? colors.gray08 : "rgba(0, 0, 0, .3)" + }`, + }, + popupIndicator: { + fontSize: fontSizes.fontSize5, + color: baseTheme.palette.text.primary, + }, + clearIndicator: { + fontSize: fontSizes.fontSize5, + color: darkMode ? colors.gray12 : baseTheme.palette.text.primary, + }, + inputRoot: ({ theme }) => ({ + paddingLeft: theme.spacing(3), + paddingRight: theme.spacing(3), + }), + tag: { + "&:first-of-type": { + marginLeft: 8, + }, + }, + option: { + "&.MuiAutocomplete-option.Mui-focused": { + backgroundColor: darkMode ? colors.blue04 : colors.blue13, + }, + }, + }, + }, + }; +}; + +export default formControls; diff --git a/ui-next/src/components/v1/theme/material/context.tsx b/ui-next/src/components/v1/theme/material/context.tsx new file mode 100644 index 0000000000..504cb53767 --- /dev/null +++ b/ui-next/src/components/v1/theme/material/context.tsx @@ -0,0 +1,13 @@ +import { PaletteMode } from "@mui/material"; +import { createContext } from "react"; + +interface ThemeProviderContext { + mode: PaletteMode; + toggler?: { + toggleColorMode: () => void; + }; +} + +export const ColorModeContext = createContext({ + mode: "light", +}); diff --git a/ui-next/src/components/v1/theme/material/provider.tsx b/ui-next/src/components/v1/theme/material/provider.tsx new file mode 100644 index 0000000000..5ad264f1dd --- /dev/null +++ b/ui-next/src/components/v1/theme/material/provider.tsx @@ -0,0 +1,32 @@ +import { PaletteMode } from "@mui/material"; +import { ThemeProvider } from "@mui/material/styles"; +import { FunctionComponent, ReactNode, useMemo, useState } from "react"; +import { getTheme } from "../theme"; +import { ColorModeContext } from "./context"; + +export const ColorModeProvider: FunctionComponent<{ + children: ReactNode; +}> = ({ children, ...rest }) => { + const [mode, setMode] = useState("light"); + const toggler = useMemo( + () => ({ + toggleColorMode: () => { + setMode((prevMode) => (prevMode === "light" ? "dark" : "light")); + }, + }), + [], + ); + + // Update the theme only if the mode changes + const lightOrDarkTheme = useMemo(() => { + return getTheme(mode); + }, [mode]); + + return ( + + + {children} + + + ); +}; diff --git a/ui-next/src/components/v1/theme/styles.ts b/ui-next/src/components/v1/theme/styles.ts new file mode 100644 index 0000000000..361efe7665 --- /dev/null +++ b/ui-next/src/components/v1/theme/styles.ts @@ -0,0 +1,46 @@ +import { SxProps, Theme } from "@mui/material"; +import { fontSizes } from "theme/tokens/variables"; +import { ConductorInputStyleProps } from "../ConductorInput"; +import { getColor } from "./theme"; + +// Calculate labelScale here to avoid circular dependency +const defaultFontSize = Number(fontSizes.fontSize3.replaceAll("px", "")); // pixel +export const labelScale = 12 / defaultFontSize; // (12px / input's fontSize) + +export const baseLabelStyle: SxProps = { + fontSize: fontSizes.fontSize3, + fontWeight: 200, + pointerEvents: "auto", + transform: `translate(12px, -9px) scale(${labelScale})`, +}; + +export const inputLabelStyle = ({ + theme, + isFocused, + error, + isInputEmpty, +}: ConductorInputStyleProps): SxProps => ({ + ...baseLabelStyle, + color: getColor({ theme, isFocused, error, isLabel: true, isInputEmpty }), + fontWeight: isFocused ? 500 : 200, + + "&.Mui-disabled": { + color: theme.palette.label.disabled, + }, +}); + +export const formHelperStyle = ({ + theme, + isFocused, + error, + isInputEmpty, +}: ConductorInputStyleProps): SxProps => ({ + fontSize: `${labelScale}em`, + color: getColor({ theme, isFocused, error, isLabel: true, isInputEmpty }), + pl: "8px", + mt: "4px", + + "&.Mui-disabled": { + color: theme.palette.input.text, + }, +}); diff --git a/ui-next/src/components/v1/theme/theme.ts b/ui-next/src/components/v1/theme/theme.ts new file mode 100644 index 0000000000..bb87c24a93 --- /dev/null +++ b/ui-next/src/components/v1/theme/theme.ts @@ -0,0 +1,91 @@ +import baseTheme from "theme/material/baseTheme"; +import appBar from "theme/material/components/appBar"; +import atoms from "theme/material/components/atoms"; +import dropdownsMenusPopovers from "theme/material/components/dropdownsMenusPopovers"; +import modals from "theme/material/components/modals"; +import paper from "theme/material/components/paper"; +import tables from "theme/material/components/tables"; +import tabs from "theme/material/components/tabs"; +import buttons from "./material/components/buttons"; +import buttonsGroup from "./material/components/buttonsGroup"; +import formControls from "./material/components/formControls"; + +import { PaletteMode } from "@mui/material"; +import { ThemeOptions } from "@mui/material/styles"; + +import { createTheme } from "@mui/material/styles"; +import { getPaletteForMode } from "theme/material/getPaletteForMode"; +import { ConductorInputStyleProps } from "../ConductorInput"; + +declare module "@mui/material/Button" { + interface ButtonPropsColorOverrides { + tertiary: true; + } +} +declare module "@mui/material/ButtonGroup" { + interface ButtonGroupPropsColorOverrides { + tertiary: true; + } +} + +export const getOverridesForMode = (mode: PaletteMode) => { + const overrides = { + components: { + ...appBar(mode), + ...paper, + // the tiniest reusables like Chip, Link, SvgIcon, etc. + ...atoms(mode), + // ALL buttons + ...buttons(mode), + // button group + ...buttonsGroup(mode), + // inputs, checkboxes, radios, textareas, autocomplete, etc. + ...formControls(mode), + // all kinds of popovers, dropdowns, toasts, snackbars, + ...dropdownsMenusPopovers(), + ...modals(mode), + ...tables, + ...tabs(mode), + }, + }; + + return overrides as ThemeOptions; +}; + +export const getTheme = (mode: PaletteMode = "light") => { + return createTheme( + baseTheme, + getOverridesForMode(mode), + getPaletteForMode(mode), + ); +}; + +export default getTheme; + +export const LOCAL_STORAGE_DARK_MODE_TOGGLE_KEY = "dark-mode-toggle"; + +export const getColor = ({ + theme, + isFocused, + error, + isLabel, + isInputEmpty, +}: ConductorInputStyleProps) => { + if (error) { + return theme.palette.input.error; + } + + if (isFocused) { + return theme.palette.input.focus; + } + + if (isLabel) { + if (isInputEmpty) { + return theme.palette.input.text; + } + + return theme.palette.input.label; + } + + return theme.palette.input.border; +}; diff --git a/ui-next/src/growthbook/MaybeGrowthbookProvider.tsx b/ui-next/src/growthbook/MaybeGrowthbookProvider.tsx new file mode 100644 index 0000000000..882f01cc57 --- /dev/null +++ b/ui-next/src/growthbook/MaybeGrowthbookProvider.tsx @@ -0,0 +1,21 @@ +import { GrowthBookProvider } from "@growthbook/growthbook-react"; +// import { gtagAbstract } from "utils/gtag"; +import { GrowthBook } from "@growthbook/growthbook"; +import { ReactNode } from "react"; +import { growthbook, isPlayground } from "./growthbookInstance"; + +export const MaybeGrowthbookProvider = ({ + children, +}: { + children: ReactNode; +}) => { + return isPlayground ? ( + >} + > + {children} + + ) : ( + children + ); +}; diff --git a/ui-next/src/growthbook/growthbookInstance.ts b/ui-next/src/growthbook/growthbookInstance.ts new file mode 100644 index 0000000000..5183f5352c --- /dev/null +++ b/ui-next/src/growthbook/growthbookInstance.ts @@ -0,0 +1,25 @@ +import { GrowthBook } from "@growthbook/growthbook"; +import { featureFlags, FEATURES } from "utils/flags"; +import { autoAttributesPlugin, thirdPartyTrackingPlugin } from "./plugins"; + +export const isPlayground = featureFlags.isEnabled(FEATURES.PLAYGROUND); + +const growthbookClientKey = featureFlags.getValue( + FEATURES.GROWTHBOOK_CLIENT_KEY, +); + +export const growthbook = isPlayground + ? new GrowthBook({ + apiHost: "https://cdn.growthbook.io", + clientKey: growthbookClientKey, + // enableDevMode: true, + plugins: [ + autoAttributesPlugin(), + thirdPartyTrackingPlugin({ trackers: ["gtm", "gtag"] }), + ], + }) + : null; + +growthbook?.init({ + streaming: true, +}); diff --git a/ui-next/src/growthbook/plugins.ts b/ui-next/src/growthbook/plugins.ts new file mode 100644 index 0000000000..4e0bd37ce8 --- /dev/null +++ b/ui-next/src/growthbook/plugins.ts @@ -0,0 +1,293 @@ +/* eslint-disable */ +import type { + GrowthBook, + UserScopedGrowthBook, + GrowthBookClient, + TrackingCallback, +} from "@growthbook/growthbook"; +/** + * This plugins are copied directly from the growthbook repo https://github.com/growthbook/growthbook + * Given they have a bug that prevents importing for those using older js modules + */ + +export type AutoAttributeSettings = { + uuidCookieName?: string; + uuidKey?: string; + uuid?: string; + uuidAutoPersist?: boolean; +}; + +function getBrowserDevice(ua: string): { browser: string; deviceType: string } { + const browser = ua.match(/Edg/) + ? "edge" + : ua.match(/Chrome/) + ? "chrome" + : ua.match(/Firefox/) + ? "firefox" + : ua.match(/Safari/) + ? "safari" + : "unknown"; + + const deviceType = ua.match(/Mobi/) ? "mobile" : "desktop"; + + return { browser, deviceType }; +} + +function getURLAttributes(url: URL | Location | undefined) { + if (!url) return {}; + return { + url: url.href, + path: url.pathname, + host: url.host, + query: url.search, + }; +} + +export function autoAttributesPlugin(settings: AutoAttributeSettings = {}) { + // Browser only + if (typeof window === "undefined") { + throw new Error("autoAttributesPlugin only works in the browser"); + } + + const COOKIE_NAME = settings.uuidCookieName || "gbuuid"; + const uuidKey = settings.uuidKey || "id"; + let uuid = settings.uuid || ""; + function persistUUID() { + setCookie(COOKIE_NAME, uuid); + } + function getUUID() { + // Already stored in memory, return + if (uuid) return uuid; + + // If cookie is already set, return + uuid = getCookie(COOKIE_NAME); + if (uuid) return uuid; + + // Generate a new UUID + uuid = genUUID(window.crypto); + return uuid; + } + + // Listen for a custom event to persist the UUID cookie + document.addEventListener("growthbookpersist", () => { + persistUUID(); + }); + + function getAutoAttributes(settings: AutoAttributeSettings) { + const ua = navigator.userAgent; + + const _uuid = getUUID(); + + // If a uuid is provided, default persist to false, otherwise default to true + if (settings.uuidAutoPersist ?? !settings.uuid) { + persistUUID(); + } + + const url = location; + + return { + ...getDataLayerVariables(), + [uuidKey]: _uuid, + ...getURLAttributes(url), + pageTitle: document.title, + ...getBrowserDevice(ua), + ...getUtmAttributes(url), + }; + } + + return (gb: GrowthBook | UserScopedGrowthBook | GrowthBookClient) => { + // Only works for instances with user attributes + if ("createScopedInstance" in gb) { + return; + } + + // Set initial attributes + const attributes = getAutoAttributes(settings); + attributes.url && gb.setURL(attributes.url); + gb.updateAttributes(attributes); + + // Poll for URL changes and update GrowthBook + let currentUrl = attributes.url; + const intervalTimer = setInterval(() => { + if (location.href !== currentUrl) { + currentUrl = location.href; + gb.setURL(currentUrl); + gb.updateAttributes(getAutoAttributes(settings)); + } + }, 500); + + // Listen for a custom event to update URL and attributes + const refreshListener = () => { + if (location.href !== currentUrl) { + currentUrl = location.href; + gb.setURL(currentUrl); + } + gb.updateAttributes(getAutoAttributes(settings)); + }; + document.addEventListener("growthbookrefresh", refreshListener); + + if ("onDestroy" in gb) { + gb.onDestroy(() => { + clearInterval(intervalTimer); + document.removeEventListener("growthbookrefresh", refreshListener); + }); + } + }; +} + +function setCookie(name: string, value: string) { + const d = new Date(); + const COOKIE_DAYS = 400; // 400 days is the max cookie duration for chrome + d.setTime(d.getTime() + 24 * 60 * 60 * 1000 * COOKIE_DAYS); + document.cookie = name + "=" + value + ";path=/;expires=" + d.toUTCString(); +} + +function getCookie(name: string): string { + const value = "; " + document.cookie; + const parts = value.split(`; ${name}=`); + return parts.length === 2 ? parts[1].split(";")[0] : ""; +} + +function genUUID(crypto: Crypto): string { + if (!crypto || (!crypto.randomUUID && !crypto.getRandomValues)) { + throw new Error("Web Crypto API is not supported in this browser."); + } + if (crypto.randomUUID) return crypto.randomUUID(); + // Fallback for browsers that have getRandomValues but not randomUUID (pre-2021) + return ("" + 1e7 + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) => { + const n = crypto.getRandomValues(new Uint8Array(1))[0]; + return ( + (c as unknown as number) ^ + (n & (15 >> ((c as unknown as number) / 4))) + ).toString(16); + }); +} + +function getUtmAttributes(url: URL | Location | undefined) { + // Store utm- params in sessionStorage for future page loads + let utms: Record = {}; + try { + const existing = sessionStorage.getItem("utm_params"); + if (existing) { + utms = JSON.parse(existing); + } + } catch (e) { + // Do nothing if sessionStorage is disabled (e.g. incognito window) + } + + // Add utm params from querystring + if (url && url.search) { + const params = new URLSearchParams(url.search); + let hasChanges = false; + ["source", "medium", "campaign", "term", "content"].forEach((k) => { + // Querystring is in snake_case + const param = `utm_${k}`; + // Attribute keys are camelCase + const attr = `utm` + k[0].toUpperCase() + k.slice(1); + + if (params.has(param)) { + utms[attr] = params.get(param) || ""; + hasChanges = true; + } + }); + + // Write back to sessionStorage + if (hasChanges) { + try { + sessionStorage.setItem("utm_params", JSON.stringify(utms)); + } catch (e) { + // Do nothing if sessionStorage is disabled (e.g. incognito window) + } + } + } + + return utms; +} + +function getDataLayerVariables() { + if ( + typeof window === "undefined" || + !window.dataLayer || + !window.dataLayer.forEach + ) { + return {}; + } + + const obj: Record = {}; + window.dataLayer.forEach((item: unknown) => { + // Skip empty and non-object entries + if (!item || typeof item !== "object" || "length" in item) return; + + // Skip events + if ("event" in item) return; + + Object.keys(item).forEach((k) => { + // Filter out known properties that aren't useful + if (typeof k !== "string" || k.match(/^(gtm)/)) return; + + const val = (item as Record)[k]; + + // Only add primitive variable values + const valueType = typeof val; + if (["string", "number", "boolean"].includes(valueType)) { + obj[k] = val; + } + }); + }); + return obj; +} + +export type Trackers = "gtag" | "gtm" | "segment"; + +export function thirdPartyTrackingPlugin({ + additionalCallback, + trackers = ["gtag", "gtm", "segment"], +}: { + additionalCallback?: TrackingCallback; + trackers?: Trackers[]; +} = {}) { + // Browser only + if (typeof window === "undefined") { + throw new Error("thirdPartyTrackingPlugin only works in the browser"); + } + + return (gb: GrowthBook | UserScopedGrowthBook | GrowthBookClient) => { + gb.setTrackingCallback(async (e, r) => { + const promises: Promise[] = []; + const eventParams = { experiment_id: e.key, variation_id: r.key }; + + if (additionalCallback) { + promises.push(Promise.resolve(additionalCallback(e, r))); + } + + // GA4 - gtag + if (trackers.includes("gtag") && window.gtag) { + let gtagResolve; + const gtagPromise = new Promise((resolve) => { + gtagResolve = resolve; + }); + promises.push(gtagPromise); + window.gtag("event", "experiment_viewed", { + ...eventParams, + event_callback: gtagResolve, + }); + } + + // GTM - dataLayer + if (trackers.includes("gtm") && window.dataLayer) { + let datalayerResolve; + const datalayerPromise = new Promise((resolve) => { + datalayerResolve = resolve; + }); + promises.push(datalayerPromise); + window?.dataLayer.push({ + event: "experiment_viewed", + ...eventParams, + eventCallback: datalayerResolve, + }); + } + + await Promise.all(promises); + }); + }; +} diff --git a/ui-next/src/growthbook/useMaybeIdentifyGrowthbook.tsx b/ui-next/src/growthbook/useMaybeIdentifyGrowthbook.tsx new file mode 100644 index 0000000000..c694edf99d --- /dev/null +++ b/ui-next/src/growthbook/useMaybeIdentifyGrowthbook.tsx @@ -0,0 +1,19 @@ +import { useEffect } from "react"; +import { logger } from "utils/logger"; +import { growthbook } from "./growthbookInstance"; + +function getGtagPseudoId() { + const match = document.cookie.match(/_ga=GA1\.\d\.(\d+)/); + return match ? match[1] : undefined; +} + +export const useMaybeIdentifyGrowthbook = () => { + useEffect(() => { + if (growthbook) { + logger.info("Initializing Growthbook"); + growthbook?.setAttributes({ + id: getGtagPseudoId(), // we use the pseudoID because we want the same experiments for the session + }); + } + }, []); +}; diff --git a/ui-next/src/images/401-Error.png b/ui-next/src/images/401-Error.png new file mode 100644 index 0000000000..c18ba3ac41 Binary files /dev/null and b/ui-next/src/images/401-Error.png differ diff --git a/ui-next/src/images/svg/banner-icon.svg b/ui-next/src/images/svg/banner-icon.svg new file mode 100644 index 0000000000..a514d9cd46 --- /dev/null +++ b/ui-next/src/images/svg/banner-icon.svg @@ -0,0 +1,36 @@ + + + + + + \ No newline at end of file diff --git a/ui-next/src/images/svg/c-plus-plus-logo.svg b/ui-next/src/images/svg/c-plus-plus-logo.svg new file mode 100644 index 0000000000..fd00504139 --- /dev/null +++ b/ui-next/src/images/svg/c-plus-plus-logo.svg @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/ui-next/src/images/svg/c-sharp-logo.svg b/ui-next/src/images/svg/c-sharp-logo.svg new file mode 100644 index 0000000000..581a3cb484 --- /dev/null +++ b/ui-next/src/images/svg/c-sharp-logo.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/ui-next/src/images/svg/discourse-logo.svg b/ui-next/src/images/svg/discourse-logo.svg new file mode 100644 index 0000000000..0f4f883f75 --- /dev/null +++ b/ui-next/src/images/svg/discourse-logo.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/ui-next/src/images/svg/email-not-verified.svg b/ui-next/src/images/svg/email-not-verified.svg new file mode 100644 index 0000000000..a01347070e --- /dev/null +++ b/ui-next/src/images/svg/email-not-verified.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-next/src/images/svg/go-lang-logo.svg b/ui-next/src/images/svg/go-lang-logo.svg new file mode 100644 index 0000000000..0aee6f06ee --- /dev/null +++ b/ui-next/src/images/svg/go-lang-logo.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-next/src/images/svg/java-logo.svg b/ui-next/src/images/svg/java-logo.svg new file mode 100644 index 0000000000..c9739a53c7 --- /dev/null +++ b/ui-next/src/images/svg/java-logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/ui-next/src/images/svg/javascript-logo.svg b/ui-next/src/images/svg/javascript-logo.svg new file mode 100644 index 0000000000..23cd8498ee --- /dev/null +++ b/ui-next/src/images/svg/javascript-logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/ui-next/src/images/svg/learning-window-test.svg b/ui-next/src/images/svg/learning-window-test.svg new file mode 100644 index 0000000000..6067fafade --- /dev/null +++ b/ui-next/src/images/svg/learning-window-test.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/ui-next/src/images/svg/news.svg b/ui-next/src/images/svg/news.svg new file mode 100644 index 0000000000..655dbd126d --- /dev/null +++ b/ui-next/src/images/svg/news.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui-next/src/images/svg/orkes-icon.svg b/ui-next/src/images/svg/orkes-icon.svg new file mode 100644 index 0000000000..775b0c4262 --- /dev/null +++ b/ui-next/src/images/svg/orkes-icon.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-next/src/images/svg/orkes-logo.svg b/ui-next/src/images/svg/orkes-logo.svg new file mode 100644 index 0000000000..d1e8b279e7 --- /dev/null +++ b/ui-next/src/images/svg/orkes-logo.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-next/src/images/svg/playIcon.svg b/ui-next/src/images/svg/playIcon.svg new file mode 100644 index 0000000000..93e2720d8e --- /dev/null +++ b/ui-next/src/images/svg/playIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui-next/src/images/svg/python-logo.svg b/ui-next/src/images/svg/python-logo.svg new file mode 100644 index 0000000000..627b597509 --- /dev/null +++ b/ui-next/src/images/svg/python-logo.svg @@ -0,0 +1,10 @@ + + + diff --git a/ui-next/src/images/svg/slack-logo-transparent.svg b/ui-next/src/images/svg/slack-logo-transparent.svg new file mode 100644 index 0000000000..e8f7ba87a3 --- /dev/null +++ b/ui-next/src/images/svg/slack-logo-transparent.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/ui-next/src/images/svg/token.svg b/ui-next/src/images/svg/token.svg new file mode 100644 index 0000000000..c7ac4f1833 --- /dev/null +++ b/ui-next/src/images/svg/token.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/ui-next/src/images/svg/user-not-found.svg b/ui-next/src/images/svg/user-not-found.svg new file mode 100644 index 0000000000..eacb5b7215 --- /dev/null +++ b/ui-next/src/images/svg/user-not-found.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-next/src/images/svg/welcome-modal.svg b/ui-next/src/images/svg/welcome-modal.svg new file mode 100644 index 0000000000..b216479667 --- /dev/null +++ b/ui-next/src/images/svg/welcome-modal.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/ui-next/src/index.css b/ui-next/src/index.css new file mode 100644 index 0000000000..f8f938babd --- /dev/null +++ b/ui-next/src/index.css @@ -0,0 +1,59 @@ +@import url("https://fonts.googleapis.com/css2?family=Gothic+A1:wght@400;500;600;700;800&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=Lexend:wght@300;400;500;600;700&display=swap"); + +body { + margin: 0; + min-height: 100vh; + font-family: + "Lexend", + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + "Roboto", + "Oxygen", + "Ubuntu", + "Cantarell", + "Fira Sans", + "Droid Sans", + "Helvetica Neue", + sans-serif !important; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + background-color: #fcfcff; /*gray13*/ + overflow: hidden; +} + +::-webkit-scrollbar { + height: 8px; + width: 8px; + background: var(--backgroundLightest); +} + +::-webkit-scrollbar-thumb { + background: var(--backgroundLighter); + -webkit-border-radius: 1ex; + /*-webkit-box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.75);*/ +} + +::-webkit-scrollbar-corner { + /*background: #000;*/ +} + +code { + font-family: + source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; +} +.rightPanel { + background-color: white; + width: 100%; + height: 100%; +} + +@keyframes rotate { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} diff --git a/ui-next/src/index.ts b/ui-next/src/index.ts new file mode 100644 index 0000000000..6736f10c77 --- /dev/null +++ b/ui-next/src/index.ts @@ -0,0 +1,200 @@ +/** + * Conductor UI - Open Source + * + * This is the main entry point for the conductor-ui npm package. + * It exports the plugin system, core components, pages, utilities, and types + * that enterprise packages can use to extend the application. + */ + +// ============================================================================= +// Plugin System - Primary export for enterprise extensions +// ============================================================================= +export { pluginRegistry } from "./plugins/registry"; +export type { + ConductorPlugin, + PluginRegistry, + // Task forms + PluginTaskFormProps, + TaskFormRegistration, + // Task menu + TaskMenuCategory, + TaskMenuItemRegistration, + // Sidebar + SidebarItemRegistration, + SidebarItemPosition, + SidebarMenuTarget, + SidebarExtension, + // Auth + AuthProviderProps, + AuthProviderRegistration, + // Search + SearchProviderRegistration, + SearchResultItem, + SearchDataFetcher, + SearchResultMapper, + // Task docs + TaskDocUrlRegistration, + // Integration modal + NewIntegrationModalProps, + // Playground + // PlaygroundHomeRegistration - not a type, handled differently + // App layout + // AppLayoutRegistration - not a type, handled differently + // Dependencies + DependencySectionProps, + DependencySectionRegistration, + WorkflowDependencies, + // Schema dialogs + SchemaEditDialogProps, + SchemaPreviewDialogProps, + // Generated key dialog + GeneratedKeyDialogProps, +} from "./plugins/registry/types"; + +// ============================================================================= +// App Shell & Routing +// ============================================================================= +export { App } from "./components/App"; +export { getRoutes } from "./routes/routes"; +export { default as AuthGuard } from "./components/auth/AuthGuard"; + +// ============================================================================= +// Core Components +// ============================================================================= +export * from "./components"; + +// Additional commonly used components +export { default as Header } from "./components/Header"; +export { default as ClipboardCopy } from "./components/ClipboardCopy"; +export { default as ConfirmChoiceDialog } from "./components/ConfirmChoiceDialog"; +export { default as NoDataComponent } from "./components/NoDataComponent"; +export { DocLink } from "./components/DocLink"; +export { SnackbarMessage } from "./components/SnackbarMessage"; +export { TagsRenderer } from "./components/v1/TagList"; +export { default as AddIcon } from "./components/v1/icons/AddIcon"; +export { default as CopyIcon } from "./components/v1/icons/CopyIcon"; +export { default as AddTagDialog } from "./components/tags/AddTagDialog"; + +// Sidebar components +export { Sidebar } from "./components/Sidebar"; +export { SidebarContext } from "./components/Sidebar/context/SidebarContext"; +export { SidebarProvider } from "./components/Sidebar/context/SidebarContextProvider"; +export { getCoreSidebarItems } from "./components/Sidebar/sidebarCoreItems"; + +// ============================================================================= +// Core Pages (for customization/extension) +// ============================================================================= +export { WorkflowSearch, SchedulerExecutions } from "./pages/executions"; +export { default as WorkflowDefinition } from "./pages/definition/WorkflowDefinition"; +export { TaskDefinition } from "./pages/definition/task"; +export { EventMonitor } from "./pages/eventMonitor/EventMonitor"; +export { default as TaskQueue } from "./pages/queueMonitor/TaskQueue"; +export { default as ErrorPage } from "./pages/error/ErrorPage"; + +// Definition pages +export { + Workflow as WorkflowDefinitions, + Task as TaskDefinitions, + EventHandler as EventHandlerDefinitions, + Schedules as ScheduleDefinitions, +} from "./pages/definitions"; + +// ============================================================================= +// Shared Utilities & Hooks +// ============================================================================= +export { useAuth } from "./shared/auth"; +export { UISidebar } from "./components/Sidebar/UiSidebar"; + +// ============================================================================= +// Auth Infrastructure (minimal stubs for OSS mode) +// Full auth implementation is in the enterprise package. +// ============================================================================= +export { authProviderMachine } from "./shared/state/machine"; +export { AuthContext } from "./shared/auth/context"; +export type { AuthState } from "./shared/auth/types"; +export { defaultAuthState } from "./shared/auth/types"; +export { + setTokenData, + getTokenData, + getAccessToken, +} from "./shared/auth/tokenManagerJotai"; +export { + SupportedProviders, + AuthMachineEventTypes, + AuthProviderStates, +} from "./shared/state/types"; +export type { + AuthProviderMachineContext, + AuthProviderMachineEvents, +} from "./shared/state/types"; + +// ============================================================================= +// Query Client (for data fetching) +// ============================================================================= +export { queryClient } from "./queryClient"; + +// ============================================================================= +// Plugin Fetch Utilities +// ============================================================================= +export { fetchWithContext, fetchContextNonHook } from "./plugins/fetch"; + +// ============================================================================= +// Feature Flags & Logger +// ============================================================================= +export { featureFlags, FEATURES, logger } from "./utils"; + +// ============================================================================= +// Theme Provider +// ============================================================================= +export { Provider as ThemeProvider } from "./theme/material/provider"; +export { MessageProvider } from "./components/v1/layout/MessageContext"; + +// ============================================================================= +// Common Constants +// ============================================================================= +export { + HOT_KEYS_SIDEBAR, + HOT_KEYS_WORKFLOW_DEFINITION, +} from "./utils/constants/common"; + +// ============================================================================= +// Route Constants +// ============================================================================= +export { + API_REFERENCE_URL, + EVENT_HANDLERS_URL, + EVENT_MONITOR_URL, + NEW_TASK_DEF_URL, + RUN_WORKFLOW_URL, + SCHEDULER_DEFINITION_URL, + SCHEDULER_EXECUTION_URL, + TAGS_DASHBOARD_URL, + TASK_DEF_URL, + TASK_QUEUE_URL, + WORKFLOW_DEFINITION_URL, + WORKFLOW_EXECUTION_URL, + // Enterprise route constants (used by enterprise plugins) + WEBHOOK_ROUTE_URL, + USER_MANAGEMENT_URL, + INTEGRATIONS_MANAGEMENT_URL, + AI_PROMPTS_MANAGEMENT_URL, + GROUP_MANAGEMENT_URL, + APPLICATION_MANAGEMENT_URL, + ROLE_MANAGEMENT_URL, + SECRETS_URL, + HUMAN_TASK_URL, + SCHEMAS_URL, + REMOTE_SERVICES_URL, + SERVICE_URL, + AUTHENTICATION_URL, + ENV_VARIABLES_URL, + WORKERS_URL, + GET_STARTED_URL, + HUB_URL, +} from "./utils/constants/route"; + +// ============================================================================= +// Types +// ============================================================================= +export * from "./types"; +export type { TaskType } from "./types"; diff --git a/ui-next/src/main.tsx b/ui-next/src/main.tsx new file mode 100644 index 0000000000..3e99c683e6 --- /dev/null +++ b/ui-next/src/main.tsx @@ -0,0 +1,56 @@ +import CssBaseline from "@mui/material/CssBaseline"; +import { inspect } from "@xstate/inspect"; +import { MessageProvider } from "components/v1/layout/MessageContext"; +import "highlight.js/styles/agate.css"; +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { HotkeysProvider } from "react-hotkeys-hook"; +import { QueryClientProvider } from "react-query"; +import { ReactQueryDevtools } from "react-query/devtools"; +import { RouterProvider } from "react-router"; +import { logger } from "utils"; +import { + HOT_KEYS_SIDEBAR, + HOT_KEYS_WORKFLOW_DEFINITION, +} from "utils/constants/common"; + +// OSS build - no enterprise plugins are registered +// Enterprise builds import and register plugins in their own main.tsx + +import { router } from "./routes/router"; +import "./index.css"; +import { queryClient } from "./queryClient"; +import { Provider as ThemeProvider } from "./theme/material/provider"; + +if (import.meta.env.VITE_XSTATE_INSPECT === "true") { + inspect({ + // options + url: "https://stately.ai/viz?inspect=1", // (default) + iframe: false, // open in new window + }); +} + +logger.log("Monitoring disabled"); + +const rootElement = document.getElementById("root"); +if (!rootElement) { + throw new Error("No root element found in index.html"); +} + +createRoot(document.getElementById("root")!).render( + + + + + + + + + + + + + , +); diff --git a/ui-next/src/pages/apiDocs/ApiReferencePage.tsx b/ui-next/src/pages/apiDocs/ApiReferencePage.tsx new file mode 100644 index 0000000000..c467e73a43 --- /dev/null +++ b/ui-next/src/pages/apiDocs/ApiReferencePage.tsx @@ -0,0 +1,37 @@ +/** + * API Reference Page + * + * Redirects to the Swagger UI for API documentation. + * This is a simple redirect component that opens the Swagger UI in the current window. + */ + +import { useEffect } from "react"; +import { Box, CircularProgress, Typography } from "@mui/material"; + +const getSwaggerUrl = () => + `//${window.location.host}/swagger-ui/index.html?configUrl=/api-docs/swagger-config#/`; + +export default function ApiReferencePage() { + useEffect(() => { + // Redirect to Swagger UI + window.location.href = getSwaggerUrl(); + }, []); + + return ( + + + + Redirecting to API Documentation... + + + ); +} diff --git a/ui-next/src/pages/creatorFlags/CreatorFlags.tsx b/ui-next/src/pages/creatorFlags/CreatorFlags.tsx new file mode 100644 index 0000000000..6710cb5563 --- /dev/null +++ b/ui-next/src/pages/creatorFlags/CreatorFlags.tsx @@ -0,0 +1,1074 @@ +import { Grid } from "@mui/material"; +import Box from "@mui/material/Box"; +import Paper from "@mui/material/Paper"; +import Dropdown from "components/Dropdown"; +import MuiButton from "components/MuiButton"; +import MuiCheckbox from "components/MuiCheckbox"; +import MuiTypography from "components/MuiTypography"; +import { RoundedInput } from "components/v1/RoundedInput"; +import { identity as _identity } from "lodash/fp"; +import { FunctionComponent, useState } from "react"; +import { FEATURES, featureFlags, tryToJson } from "utils"; +import { useLocalStorage } from "utils/localstorage"; + +type FeatureFlagValue = string | boolean | null; + +type BaseFeatureFlag = { + name: string; + label: string; + contextValue: string; +}; + +type StringFeatureFlag = BaseFeatureFlag & { + type: "string"; + value: string | null; + setValue: (value: string | null) => void; +}; + +type BooleanFeatureFlag = BaseFeatureFlag & { + type: "boolean"; + value: boolean | null; + setValue: (value: boolean | null) => void; +}; + +type _FeatureFlagsType = StringFeatureFlag | BooleanFeatureFlag; + +const dropdownBorder = (value: FeatureFlagValue): string => { + if (value === null) { + return "2px solid default"; + } else if (value) { + return "2px solid forestgreen"; + } else { + return "2px solid red"; + } +}; + +type InputStringTemplateProps = StringFeatureFlag & { + handleStringChange: ( + value: string | undefined, + setStateCb: (value: string | null) => void, + ) => void; +}; + +const InputStringTemplate = ({ + label, + value, + setValue, + type: _type, + contextValue, + handleStringChange, +}: InputStringTemplateProps) => { + const [newValue, setNewValue] = useState(value || ""); + const handleNewValue = (val: string) => { + setNewValue(val); + }; + return ( + + + {label} + + + + handleNewValue(value)} + /> + + + handleStringChange(newValue, setValue)} + > + Store + + handleStringChange(undefined, setValue)} + > + Remove + + + + + + + + + + + ); +}; + +type InputCheckboxTemplateProps = BooleanFeatureFlag & { + handleChangeDropdown: ( + value: unknown, + setStateCb: (value: boolean | null) => void, + ) => void; +}; + +const InputCheckboxTemplate = ({ + label, + value, + setValue, + contextValue, + handleChangeDropdown, +}: InputCheckboxTemplateProps) => { + return ( + + + {label} + + + + handleChangeDropdown(val, setValue)} + /> + + + + + + + + + ); +}; + +export const CreatorFlags: FunctionComponent = () => { + const [withTaskStats, setTaskStats] = useLocalStorage( + FEATURES.DISABLE_TASK_STATS, + null, + ); + + const [javascriptOption, setJavascriptOption] = useLocalStorage( + FEATURES.HIDE_JAVASCRIPT_OPTION, + null, + ); + + //boolean + + const [enableTaskDefinitionForm, setEnableTaskDefinitionForm] = + useLocalStorage(FEATURES.ENABLE_TASK_DEFINITION_FORM, null); + + const [disableExpandWorkflow, setDisableExpandWorkflow] = useLocalStorage( + FEATURES.DISABLE_EXPAND_WORKFLOW, + null, + ); + + const [accessManagement, setAccessManagement] = useLocalStorage( + FEATURES.ACCESS_MANAGEMENT, + null, + ); + const [copyToken, setCopyToken] = useLocalStorage(FEATURES.COPY_TOKEN, null); + const [playground, setPlayground] = useLocalStorage( + FEATURES.PLAYGROUND, + null, + ); + const [scheduler, setScheduler] = useLocalStorage(FEATURES.SCHEDULER, null); + const [creatorEnableCreator, setCreatorEnableCreator] = useLocalStorage( + FEATURES.CREATOR_ENABLE_CREATOR, + null, + ); + const [creatorEnableReaflowDiagram, setCreatorEnableReaflowDiagram] = + useLocalStorage(FEATURES.CREATOR_ENABLE_REAFLOW_DIAGRAM, null); + + const [enableDarkmodeToggle, setEnableDarkmodeToggle] = useLocalStorage( + FEATURES.ENABLE_DARK_MODE_TOGGLE, + null, + ); + + const [navbarElementsVariant, setNavbarElementsVariant] = useLocalStorage( + FEATURES.NAVBAR_ELEMENTS_VARIANT, + null, + ); + + const [showStartTitle, setShowStartTitle] = useLocalStorage( + FEATURES.SHOW_START_TITLE, + null, + ); + + const [showCloudLink, setShowCloudLink] = useLocalStorage( + FEATURES.SHOW_CLOUD_LINK, + null, + ); + + const [showFeedbackForm, setShowFeedbackForm] = useLocalStorage( + FEATURES.SHOW_FEEDBACK_FORM, + null, + ); + + const [showSupportForm, setShowSupportForm] = useLocalStorage( + FEATURES.SHOW_SUPPORT_FORM, + null, + ); + + const [showDocumentation, setShowDocumentation] = useLocalStorage( + FEATURES.SHOW_DOCUMENTATION, + null, + ); + + const [showJoinSlackCommunity, setShowJoinSlackCommunity] = useLocalStorage( + FEATURES.SHOW_JOIN_SLACK_COMMUNITY, + null, + ); + + const [betaKeyboardFlow, setBetaKeyboardFlow] = useLocalStorage( + FEATURES.BETA_KEYBOARD_FLOW, + null, + ); + + const [enableMetricsDashboard, setEnableMetricsDashboard] = useLocalStorage( + FEATURES.ENABLE_METRICS_DASHBOARD, + null, + ); + + const [humanTask, setHumanTask] = useLocalStorage(FEATURES.HUMAN_TASK, null); + + const [integrations, setIntegrations] = useLocalStorage( + FEATURES.INTEGRATIONS, + null, + ); + + const [showNewsIcon, setShowNewsIcon] = useLocalStorage( + FEATURES.SHOW_NEWS_ICON, + null, + ); + + const [showOnBoardingQuiz, setShowOnBoardingQuiz] = useLocalStorage( + FEATURES.SHOW_ONBOARDING_QUIZ, + null, + ); + + const [envIsProduction, setEnvIsProduction] = useLocalStorage( + FEATURES.ENV_IS_PRODUCTION, + null, + ); + + const [taskIndexing, setTaskIndexing] = useLocalStorage( + FEATURES.TASK_INDEXING, + null, + ); + + const [remoteServices, setRemoteServices] = useLocalStorage( + FEATURES.REMOTE_SERVICES, + null, + ); + + const [sendgridTaskEnabled, setSendgridTaskEnabled] = useLocalStorage( + FEATURES.SENDGRID_TASK, + null, + ); + + const [aiPromptsVersioning, setAiPromptsVersioning] = useLocalStorage( + FEATURES.AI_PROMPTS_VERSIONING, + null, + ); + + const [ + advancedErrorInspectorValidations, + setAdvancedErrorInspectorValidations, + ] = useLocalStorage(FEATURES.ADVANCED_ERROR_INSPECTOR_VALIDATIONS, null); + + const [enableWhiteBackgroundForm, setEnableWhiteBackgroundForm] = + useLocalStorage(FEATURES.ENABLE_WHITE_BACKGROUND_FORM, null); + + //string + const [taskVisibility, setTaskVisibility] = useLocalStorage( + FEATURES.TASK_VISIBILITY, + _identity(), + { + code: _identity, + parse: _identity, + }, + ); + + const [metricsOriginUrl, setMetricsOriginUrl] = useLocalStorage( + FEATURES.METRICS_ORIGIN_URL, + _identity(), + { + code: _identity, + parse: _identity, + }, + ); + + const [loginRedirectType, setLoginRedirectType] = useLocalStorage( + FEATURES.LOGIN_REDIRECT_TYPE, + _identity(), + { + code: _identity, + parse: _identity, + }, + ); + + const [dragdropThreshold, setDragdropThreshold] = useLocalStorage( + FEATURES.DRAG_DROP_TASK_INCREMENT_THRESHOLD, + _identity(), + { + code: _identity, + parse: _identity, + }, + ); + + const [announcementExpiryDate, setAnnouncementExpiryDate] = useLocalStorage( + FEATURES.ANNOUNCEMENT_EXPIRY_DATE, + _identity(), + { + code: _identity, + parse: _identity, + }, + ); + + const [logRocketKey, setLogRocketKey] = useLocalStorage( + FEATURES.LOG_ROCKET_KEY, + _identity(), + { + code: _identity, + parse: _identity, + }, + ); + + const [customLogoUrl, setCustomLogoUrl] = useLocalStorage( + FEATURES.CUSTOM_LOGO_URL, + _identity(), + { + code: _identity, + parse: _identity, + }, + ); + + const [growthbookClientKey, setGrowthbookClientKey] = useLocalStorage( + FEATURES.GROWTHBOOK_CLIENT_KEY, + _identity(), + { + code: _identity, + parse: _identity, + }, + ); + + const [multiTenancyType, setMultiTenancyType] = useLocalStorage( + FEATURES.MULTITENANCY_TYPE, + _identity(), + { + code: _identity, + parse: _identity, + }, + ); + + const [defaultRoles, setDefaultRoles] = useLocalStorage( + FEATURES.DEFAULT_ROLES, + _identity(), + { + code: _identity, + parse: _identity, + }, + ); + + const [showEndTimeInDatepicker, setShowEndTimeInDatePicker] = useLocalStorage( + FEATURES.SHOW_END_TIME_IN_DATEPICKER, + null, + ); + + const [showEventMonitor, setShowEventMonitor] = useLocalStorage( + FEATURES.SHOW_EVENT_MONITOR, + null, + ); + const [showAiStudioBannerFlag, setShowAiStudioBannerFlag] = useLocalStorage( + FEATURES.SHOW_AI_STUDIO_BANNER_FLAG, + null, + ); + const [showGetStartedPage, setShowGetStartedPage] = useLocalStorage( + FEATURES.SHOW_GET_STARTED_PAGE, + null, + ); + const [gatewayEnabled, setGatewayEnabled] = useLocalStorage( + FEATURES.GATEWAY_ENABLED, + null, + ); + const [ + enableRerunFromForkAndDowhileTasks, + setEnableRerunFromForkAndDowhileTasks, + ] = useLocalStorage(FEATURES.ENABLE_RERUN_FROM_FORK_AND_DOWHILE_TASKS, null); + const [getStartedVideoUrl, setGetStartedVideoUrl] = useLocalStorage( + FEATURES.GET_STARTED_VIDEO_URL, + null, + ); + const [hideImportBpmn, setHideImportBpmn] = useLocalStorage( + FEATURES.HIDE_IMPORT_BPMN, + null, + ); + const [cloudTemplatesSource, setCloudTemplatesSource] = useLocalStorage( + FEATURES.CLOUD_TEMPLATES_SOURCE, + null, + ); + + const [showRolesMenuItem, setShowRolesMenuItem] = useLocalStorage( + FEATURES.SHOW_ROLES_MENU_ITEM, + null, + ); + + const [notifyHumanTask, setNotifyHumanTask] = useLocalStorage( + FEATURES.NOTIFY_HUMAN_TASK, + null, + ); + const [workflowIntrospection, setWorkflowIntrospection] = useLocalStorage( + FEATURES.WORKFLOW_INTROSPECTION, + null, + ); + const [enableConfetti, setEnableConfetti] = useLocalStorage( + FEATURES.ENABLE_CONFETTI, + null, + ); + const [showAgent, setShowAgent] = useLocalStorage(FEATURES.SHOW_AGENT, null); + const [enableAgentAudioInput, setEnableAgentAudioInput] = useLocalStorage( + FEATURES.ENABLE_AGENT_AUDIO_INPUT, + null, + ); + const [aiCoderCloudWorker, setAiCoderCloudWorker] = useLocalStorage( + FEATURES.AI_CODER_CLOUD_WORKER, + null, + ); + + const handleStringChange = ( + value: string | undefined, + setStateCb: (value: string | null) => void, + ) => { + setStateCb(value || null); + window.location.reload(); + }; + + const handleChangeDropdown = ( + value: unknown, + setStateCb: (value: boolean | null) => void, + ) => { + if (!value) { + setStateCb(null); + window.location.reload(); + return; + } + const stringValue = Array.isArray(value) ? String(value[0]) : String(value); + const variable: boolean | null = + stringValue === "true" ? true : stringValue === "false" ? false : null; + setStateCb(variable); + window.location.reload(); + }; + + const featureFlagsArray: (StringFeatureFlag | BooleanFeatureFlag)[] = [ + { + name: "enable_task_stats", + label: "Enable task stats", + value: withTaskStats, + contextValue: featureFlags.getContextValue(FEATURES.DISABLE_TASK_STATS), + setValue: setTaskStats, + type: "boolean", + }, + + { + name: "hide_javascript_option", + label: "Hide Javascript Option", + value: javascriptOption, + setValue: setJavascriptOption, + contextValue: featureFlags.getContextValue( + FEATURES.HIDE_JAVASCRIPT_OPTION, + ), + type: "boolean", + }, + { + name: "access_management", + label: "Access Management", + value: accessManagement, + setValue: setAccessManagement, + contextValue: featureFlags.getContextValue(FEATURES.ACCESS_MANAGEMENT), + type: "boolean", + }, + { + name: "copy_token", + label: "Copy Token", + value: copyToken, + setValue: setCopyToken, + contextValue: featureFlags.getContextValue(FEATURES.COPY_TOKEN), + type: "boolean", + }, + { + name: "playground", + label: "Playground", + value: playground, + setValue: setPlayground, + contextValue: featureFlags.getContextValue(FEATURES.PLAYGROUND), + type: "boolean", + }, + { + name: "scheduler", + label: "Scheduler", + value: scheduler, + setValue: setScheduler, + contextValue: featureFlags.getContextValue(FEATURES.SCHEDULER), + type: "boolean", + }, + { + name: "creator_enable_creator", + label: "Creator Enable Creator", + value: creatorEnableCreator, + setValue: setCreatorEnableCreator, + contextValue: featureFlags.getContextValue( + FEATURES.CREATOR_ENABLE_CREATOR, + ), + type: "boolean", + }, + { + name: "creator_enable_reaflow_diagram", + label: "Creator Enable Reaflow Diagram", + value: creatorEnableReaflowDiagram, + setValue: setCreatorEnableReaflowDiagram, + contextValue: featureFlags.getContextValue( + FEATURES.CREATOR_ENABLE_REAFLOW_DIAGRAM, + ), + type: "boolean", + }, + { + name: "enable_dark_mode_toggle", + label: "Enable Dark Mode Toggle", + value: enableDarkmodeToggle, + setValue: setEnableDarkmodeToggle, + contextValue: featureFlags.getContextValue( + FEATURES.ENABLE_DARK_MODE_TOGGLE, + ), + type: "boolean", + }, + { + name: "navbar_elements_variant", + label: "Navbar Elements Variant", + value: navbarElementsVariant, + setValue: setNavbarElementsVariant, + contextValue: featureFlags.getContextValue( + FEATURES.NAVBAR_ELEMENTS_VARIANT, + ), + type: "boolean", + }, + { + name: "show_start_title", + label: "Show Start Title", + value: showStartTitle, + setValue: setShowStartTitle, + contextValue: featureFlags.getContextValue(FEATURES.SHOW_START_TITLE), + type: "boolean", + }, + { + name: "show_cloud_link", + label: "Show Cloud Link", + value: showCloudLink, + setValue: setShowCloudLink, + contextValue: featureFlags.getContextValue(FEATURES.SHOW_CLOUD_LINK), + type: "boolean", + }, + { + name: "show_feedback_form", + label: "Show Feedback Form", + value: showFeedbackForm, + setValue: setShowFeedbackForm, + contextValue: featureFlags.getContextValue(FEATURES.SHOW_FEEDBACK_FORM), + type: "boolean", + }, + { + name: "show_support_form", + label: "Show Support Form", + value: showSupportForm, + setValue: setShowSupportForm, + contextValue: featureFlags.getContextValue(FEATURES.SHOW_SUPPORT_FORM), + type: "boolean", + }, + { + name: "show_documentation", + label: "Show Documentation", + value: showDocumentation, + setValue: setShowDocumentation, + contextValue: featureFlags.getContextValue(FEATURES.SHOW_DOCUMENTATION), + type: "boolean", + }, + { + name: "show_join_slack_community", + label: "Show Join Slack Community", + value: showJoinSlackCommunity, + setValue: setShowJoinSlackCommunity, + contextValue: featureFlags.getContextValue( + FEATURES.SHOW_JOIN_SLACK_COMMUNITY, + ), + type: "boolean", + }, + { + name: "beta_keyboard_flow", + label: "Beta Keyboard Flow", + value: betaKeyboardFlow, + setValue: setBetaKeyboardFlow, + contextValue: featureFlags.getContextValue(FEATURES.BETA_KEYBOARD_FLOW), + type: "boolean", + }, + { + name: "enable_metrics_dashboard", + label: "Enable Metrics Dashboard", + value: enableMetricsDashboard, + setValue: setEnableMetricsDashboard, + contextValue: featureFlags.getContextValue( + FEATURES.ENABLE_METRICS_DASHBOARD, + ), + type: "boolean", + }, + { + name: "human_task", + label: "Human Task", + value: humanTask, + setValue: setHumanTask, + contextValue: featureFlags.getContextValue(FEATURES.HUMAN_TASK), + type: "boolean", + }, + { + name: "intgrations", + label: "Integrations", + value: integrations, + setValue: setIntegrations, + contextValue: featureFlags.getContextValue(FEATURES.INTEGRATIONS), + type: "boolean", + }, + { + name: "show_news_icon", + label: "Show News Icon", + value: showNewsIcon, + setValue: setShowNewsIcon, + contextValue: featureFlags.getContextValue(FEATURES.SHOW_NEWS_ICON), + type: "boolean", + }, + { + name: "enable_task_definition_form", + label: "Enable Task Definition Form", + value: enableTaskDefinitionForm, + setValue: setEnableTaskDefinitionForm, + contextValue: featureFlags.getContextValue( + FEATURES.ENABLE_TASK_DEFINITION_FORM, + ), + type: "boolean", + }, + { + name: "disable_expand_workflow", + label: "Disable Expand Workflow", + value: disableExpandWorkflow, + setValue: setDisableExpandWorkflow, + contextValue: featureFlags.getContextValue( + FEATURES.DISABLE_EXPAND_WORKFLOW, + ), + type: "boolean", + }, + { + name: "show_onboarding_quiz", + label: "Show Onboarding Quiz", + value: showOnBoardingQuiz, + setValue: setShowOnBoardingQuiz, + contextValue: featureFlags.getContextValue(FEATURES.SHOW_ONBOARDING_QUIZ), + type: "boolean", + }, + { + name: "task_indexing", + label: "Task Indexing", + value: taskIndexing, + contextValue: featureFlags.getContextValue(FEATURES.TASK_INDEXING), + setValue: setTaskIndexing, + type: "boolean", + }, + { + name: "enable_white_background_form", + label: "Enable white background form", + value: enableWhiteBackgroundForm, + contextValue: featureFlags.getContextValue( + FEATURES.ENABLE_WHITE_BACKGROUND_FORM, + ), + setValue: setEnableWhiteBackgroundForm, + type: "boolean", + }, + { + name: "env_is_production", + label: "Env is Production", + value: envIsProduction, + contextValue: featureFlags.getContextValue(FEATURES.ENV_IS_PRODUCTION), + setValue: setEnvIsProduction, + type: "boolean", + }, + { + name: "advanced_error_inspector_validations", + label: "Advanced Error Inspector Validations", + value: advancedErrorInspectorValidations, + contextValue: featureFlags.getContextValue( + FEATURES.ADVANCED_ERROR_INSPECTOR_VALIDATIONS, + ), + setValue: setAdvancedErrorInspectorValidations, + type: "boolean", + }, + { + name: "remote_services", + label: "Remote Services", + value: remoteServices, + contextValue: featureFlags.getContextValue(FEATURES.REMOTE_SERVICES), + setValue: setRemoteServices, + type: "boolean", + }, + { + name: "enable_rerun_from_fork_and_dowhile_tasks", + label: "Enable Rerun From Fork And Dowhile Tasks", + value: enableRerunFromForkAndDowhileTasks, + contextValue: featureFlags.getContextValue( + FEATURES.ENABLE_RERUN_FROM_FORK_AND_DOWHILE_TASKS, + ), + setValue: setEnableRerunFromForkAndDowhileTasks, + type: "boolean", + }, + { + name: "task_visibility", + label: "Task Visibility", + value: taskVisibility, + setValue: setTaskVisibility, + contextValue: featureFlags.getContextValue(FEATURES.TASK_VISIBILITY), + type: "string", + }, + { + name: "metrics_origin_url", + label: "Metrics Origin URL", + value: metricsOriginUrl, + setValue: setMetricsOriginUrl, + contextValue: featureFlags.getContextValue(FEATURES.METRICS_ORIGIN_URL), + type: "string", + }, + { + name: "login_redirect_type", + label: "Login Redirect Type", + value: loginRedirectType, + setValue: setLoginRedirectType, + contextValue: featureFlags.getContextValue(FEATURES.LOGIN_REDIRECT_TYPE), + type: "string", + }, + { + name: "drag_drop_task_increment_threshold", + label: "Drag Drop Task Increment Threshold", + value: dragdropThreshold, + setValue: setDragdropThreshold, + contextValue: featureFlags.getContextValue( + FEATURES.DRAG_DROP_TASK_INCREMENT_THRESHOLD, + ), + type: "string", + }, + { + name: "announcement_expiry_date", + label: "Announcement Expiry Date", + value: announcementExpiryDate, + setValue: setAnnouncementExpiryDate, + contextValue: featureFlags.getContextValue( + FEATURES.ANNOUNCEMENT_EXPIRY_DATE, + ), + type: "string", + }, + { + name: "log_rocket_key", + label: "Log Rocket Key", + value: logRocketKey, + setValue: setLogRocketKey, + contextValue: featureFlags.getContextValue(FEATURES.LOG_ROCKET_KEY), + type: "string", + }, + { + name: "growthbook_client_key", + label: "Growthbook Client Key", + value: growthbookClientKey, + setValue: setGrowthbookClientKey, + contextValue: featureFlags.getContextValue( + FEATURES.GROWTHBOOK_CLIENT_KEY, + ), + type: "string", + }, + { + name: "custom_logo_url", + label: "Custom logo URL", + value: customLogoUrl, + setValue: setCustomLogoUrl, + contextValue: featureFlags.getContextValue(FEATURES.CUSTOM_LOGO_URL), + type: "string", + }, + { + name: "multi_tenancy_type", + label: "Multi-tenancy Type", + value: multiTenancyType, + setValue: setMultiTenancyType, + contextValue: featureFlags.getContextValue(FEATURES.MULTITENANCY_TYPE), + type: "string", + }, + { + name: "default_roles", + label: "Default Roles", + value: defaultRoles, + setValue: setDefaultRoles, + contextValue: featureFlags.getContextValue(FEATURES.DEFAULT_ROLES), + type: "string", + }, + { + name: "show_end_time_in_date_picker", + label: "Show End Time In Date Picker", + value: showEndTimeInDatepicker, + setValue: setShowEndTimeInDatePicker, + contextValue: featureFlags.getContextValue( + FEATURES.SHOW_END_TIME_IN_DATEPICKER, + ), + type: "boolean", + }, + { + name: "show_event_monitor", + label: "Show Event Monitor", + value: showEventMonitor, + setValue: setShowEventMonitor, + contextValue: featureFlags.getContextValue(FEATURES.SHOW_EVENT_MONITOR), + type: "boolean", + }, + { + name: "show_ai_studio_banner", + label: "Show AI Studio Banner", + value: showAiStudioBannerFlag, + setValue: setShowAiStudioBannerFlag, + contextValue: featureFlags.getContextValue( + FEATURES.SHOW_AI_STUDIO_BANNER_FLAG, + ), + type: "boolean", + }, + { + name: "show_get_started_page", + label: "Show Get Started Page", + value: showGetStartedPage, + setValue: setShowGetStartedPage, + contextValue: featureFlags.getContextValue( + FEATURES.SHOW_GET_STARTED_PAGE, + ), + type: "boolean", + }, + { + name: "gateway_enabled", + label: "Gateway Enabled", + value: gatewayEnabled, + setValue: setGatewayEnabled, + contextValue: featureFlags.getContextValue(FEATURES.GATEWAY_ENABLED), + type: "boolean", + }, + { + name: "get_started_video_url", + label: "Get Started Page Video URL", + value: getStartedVideoUrl, + setValue: setGetStartedVideoUrl, + contextValue: featureFlags.getContextValue( + FEATURES.GET_STARTED_VIDEO_URL, + ), + type: "string", + }, + { + name: "sendgrid_task", + label: "Sendgrid Task", + value: sendgridTaskEnabled, + contextValue: featureFlags.getContextValue(FEATURES.SENDGRID_TASK), + setValue: setSendgridTaskEnabled, + type: "boolean", + }, + { + name: "ai_prompts_versioning", + label: "AI Prompts Versioning", + value: aiPromptsVersioning, + setValue: setAiPromptsVersioning, + contextValue: featureFlags.getContextValue( + FEATURES.AI_PROMPTS_VERSIONING, + ), + type: "boolean", + }, + { + name: "hide_import_bpmn", + label: "Hide Import BPMN", + value: hideImportBpmn, + setValue: setHideImportBpmn, + contextValue: featureFlags.getContextValue(FEATURES.HIDE_IMPORT_BPMN), + type: "boolean", + }, + { + name: "cloud_templates_source", + label: "Cloud Templates Source", + value: cloudTemplatesSource, + setValue: setCloudTemplatesSource, + contextValue: featureFlags.getContextValue( + FEATURES.CLOUD_TEMPLATES_SOURCE, + ), + type: "string", + }, + { + name: "show_roles_menu_item", + label: "Show Roles Menu Item", + value: showRolesMenuItem, + setValue: setShowRolesMenuItem, + contextValue: featureFlags.getContextValue(FEATURES.SHOW_ROLES_MENU_ITEM), + type: "boolean", + }, + { + name: "notify_human_task", + label: "Notify Human Task", + value: notifyHumanTask, + setValue: setNotifyHumanTask, + contextValue: featureFlags.getContextValue(FEATURES.NOTIFY_HUMAN_TASK), + type: "boolean", + }, + { + name: "workflow_introspection", + label: "Workflow Introspection", + value: workflowIntrospection, + setValue: setWorkflowIntrospection, + contextValue: featureFlags.getContextValue( + FEATURES.WORKFLOW_INTROSPECTION, + ), + type: "boolean", + }, + { + name: "enable_confetti", + label: "Enable Confetti", + value: enableConfetti, + setValue: setEnableConfetti, + contextValue: featureFlags.getContextValue(FEATURES.ENABLE_CONFETTI), + type: "boolean", + }, + { + name: "workflow_introspection", + label: "Workflow Introspection", + value: workflowIntrospection, + setValue: setWorkflowIntrospection, + contextValue: featureFlags.getContextValue( + FEATURES.WORKFLOW_INTROSPECTION, + ), + type: "boolean", + }, + { + name: "show_agent", + label: "Show Agent", + value: showAgent, + setValue: setShowAgent, + contextValue: featureFlags.getContextValue(FEATURES.SHOW_AGENT), + type: "boolean", + }, + { + name: "enable_agent_audio_input", + label: "Enable Agent Audio Input", + value: enableAgentAudioInput, + setValue: setEnableAgentAudioInput, + contextValue: featureFlags.getContextValue( + FEATURES.ENABLE_AGENT_AUDIO_INPUT, + ), + type: "boolean", + }, + // AI_CODER_CLOUD_WORKER + { + name: "ai_coder_cloud_worker", + label: "AI Coder Cloud Worker", + value: aiCoderCloudWorker, + setValue: setAiCoderCloudWorker, + contextValue: featureFlags.getContextValue( + FEATURES.AI_CODER_CLOUD_WORKER, + ), + type: "boolean", + }, + ]; + + const headerStyle = { + fontSize: "14px", + fontWeight: 600, + }; + + const renderFlagFields = ( + items: (StringFeatureFlag | BooleanFeatureFlag)[], + ) => + items.map((item) => ( + + {item.type === "boolean" ? ( + + ) : ( + + )} + + )); + + return ( +
    + + Beta Feature flags + + *LocalStorage take precedence over flags defined in window.conductor + or process.env + + + + + Flag + + + Local Storage + + + Context + + + + {renderFlagFields(featureFlagsArray)} + +
    + ); +}; diff --git a/ui-next/src/pages/definition/ConfirmDialog.tsx b/ui-next/src/pages/definition/ConfirmDialog.tsx new file mode 100644 index 0000000000..f1ce6fe643 --- /dev/null +++ b/ui-next/src/pages/definition/ConfirmDialog.tsx @@ -0,0 +1,30 @@ +import { useCallback, FunctionComponent } from "react"; +import ConfirmChoiceDialog from "components/ConfirmChoiceDialog"; + +interface ConfirmDialogProps { + onConfirm: () => void; + onCancel: () => void; + shouldPrompt: boolean; + message: string; + title?: string; +} + +export const ConfirmDialog: FunctionComponent = ({ + onConfirm, + onCancel, + shouldPrompt, + title = "Confirmation", + message, +}) => { + const handleConfirmUseLocalChanges = useCallback( + (val: boolean) => (val ? onConfirm : onCancel)(), + [onConfirm, onCancel], + ); + return shouldPrompt ? ( + + ) : null; +}; diff --git a/ui-next/src/pages/definition/ConfirmLocalCopyDialog/ConfirmLocalCopyDialog.tsx b/ui-next/src/pages/definition/ConfirmLocalCopyDialog/ConfirmLocalCopyDialog.tsx new file mode 100644 index 0000000000..1d738deeed --- /dev/null +++ b/ui-next/src/pages/definition/ConfirmLocalCopyDialog/ConfirmLocalCopyDialog.tsx @@ -0,0 +1,65 @@ +import { useCallback, FunctionComponent, useMemo } from "react"; +import { useSelector, useActor } from "@xstate/react"; +import { ActorRef } from "xstate"; +import ConfirmChoiceDialog from "components/ConfirmChoiceDialog"; +import { LocalCopyMachineEvents, LocalCopyMachineEventTypes } from "./state"; +import { + getLocalCopyTime, + extractKeyFromContext, +} from "pages/runWorkflow/runWorkflowUtils"; + +interface ConfirmLocalCopyDialogProps { + localCopyActor: ActorRef; +} + +export const ConfirmLocalCopyDialog: FunctionComponent< + ConfirmLocalCopyDialogProps +> = ({ localCopyActor }) => { + const [, send] = useActor(localCopyActor); + const isPromptUseLocalCopy = useSelector(localCopyActor, (state) => + state.matches("promptUseLocalCopy"), + ); + + const isNewWorkflow = useSelector( + localCopyActor, + (state) => state.context.isNewWorkflow, + ); + const { workflowName, currentVersion } = useSelector( + localCopyActor, + (state) => state.context, + ); + + const maybeLocalCopyUpdateTime = getLocalCopyTime( + extractKeyFromContext({ workflowName, currentVersion }), + ); + const localCopySaveTime = useMemo( + () => + isPromptUseLocalCopy && + isNewWorkflow === false && + maybeLocalCopyUpdateTime != null + ? ` (Last saved on : ${maybeLocalCopyUpdateTime})` + : "", + [isPromptUseLocalCopy, isNewWorkflow, maybeLocalCopyUpdateTime], + ); + + const handleConfirmUseLocalChanges = useCallback( + (val: boolean) => + send({ + type: val + ? LocalCopyMachineEventTypes.USE_LOCAL_CHANGES_EVT + : LocalCopyMachineEventTypes.CANCEL_EVENT_EVT, + } as LocalCopyMachineEvents), + [send], + ); + return isPromptUseLocalCopy ? ( + + ) : null; +}; diff --git a/ui-next/src/pages/definition/ConfirmLocalCopyDialog/state/actions.ts b/ui-next/src/pages/definition/ConfirmLocalCopyDialog/state/actions.ts new file mode 100644 index 0000000000..817aea609e --- /dev/null +++ b/ui-next/src/pages/definition/ConfirmLocalCopyDialog/state/actions.ts @@ -0,0 +1,29 @@ +import { WorkflowDef } from "types/WorkflowDef"; +import { assign, DoneInvokeEvent, sendParent } from "xstate"; +import { LocalCopyMachineContext, LocalCopyMachineEventTypes } from "./types"; +import { WorkflowWithNoErrorsEvent } from "../../errorInspector/state"; + +export const storeLocalCopy = assign< + LocalCopyMachineContext, + DoneInvokeEvent> +>({ + lastStoredVersion: (_ctxt, event) => event.data, +}); + +export const sendLocalChanges = sendParent( + (context) => ({ + type: LocalCopyMachineEventTypes.USE_LOCAL_COPY_WORKFLOW, + workflow: context.lastStoredVersion, + }), +); + +export const persistLastStoredVersion = assign< + LocalCopyMachineContext, + WorkflowWithNoErrorsEvent +>((__context, { workflow }) => ({ + lastStoredVersion: workflow, +})); + +export const cleanLocalChanges = assign({ + lastStoredVersion: (__context) => undefined, +}); diff --git a/ui-next/src/pages/definition/ConfirmLocalCopyDialog/state/hook.ts b/ui-next/src/pages/definition/ConfirmLocalCopyDialog/state/hook.ts new file mode 100644 index 0000000000..6889880e14 --- /dev/null +++ b/ui-next/src/pages/definition/ConfirmLocalCopyDialog/state/hook.ts @@ -0,0 +1,15 @@ +import { ActorRef } from "xstate"; +import { LocalCopyMachineEvents, LocalCopyMachineEventTypes } from "./types"; +export const useLocalCopyMachine = ( + service: ActorRef, +) => { + const handleRemoveLocalCopyMessage = () => + service.send({ + type: LocalCopyMachineEventTypes.REMOVE_LOCAL_COPY_MESSAGE, + }); + return [ + { + handleRemoveLocalCopyMessage, + }, + ]; +}; diff --git a/ui-next/src/pages/definition/ConfirmLocalCopyDialog/state/index.ts b/ui-next/src/pages/definition/ConfirmLocalCopyDialog/state/index.ts new file mode 100644 index 0000000000..566a3c4dbe --- /dev/null +++ b/ui-next/src/pages/definition/ConfirmLocalCopyDialog/state/index.ts @@ -0,0 +1,3 @@ +export * from "./machine"; +export * from "./types"; +export * from "./service"; diff --git a/ui-next/src/pages/definition/ConfirmLocalCopyDialog/state/machine.ts b/ui-next/src/pages/definition/ConfirmLocalCopyDialog/state/machine.ts new file mode 100644 index 0000000000..8a1bf18241 --- /dev/null +++ b/ui-next/src/pages/definition/ConfirmLocalCopyDialog/state/machine.ts @@ -0,0 +1,81 @@ +import { createMachine } from "xstate"; +import * as services from "./service"; +import * as actions from "./actions"; +import { + LocalCopyMachineContext, + LocalCopyMachineEvents, + LocalCopyMachineEventTypes, +} from "./types"; +import _isEmpty from "lodash/isEmpty"; + +export const localCopyMachine = createMachine< + LocalCopyMachineContext, + LocalCopyMachineEvents +>( + { + predictableActionArguments: true, + id: "localCopyMachine", + initial: "lookForLocalCopies", + context: { + currentVersion: undefined, + isNewWorkflow: false, + workflowName: "", + lastStoredVersion: {}, + currentWf: {}, + }, + states: { + lookForLocalCopies: { + invoke: { + src: "consumeCopyFromLocalStorage", + onDone: [ + { + cond: (context) => context.isNewWorkflow, + actions: ["storeLocalCopy"], + target: "finish", + }, + { + actions: ["storeLocalCopy"], + target: "promptUseLocalCopy", + }, + ], + onError: { + target: "finish", + }, + }, + }, + promptUseLocalCopy: { + on: { + [LocalCopyMachineEventTypes.USE_LOCAL_CHANGES_EVT]: { + target: "finish", + }, + [LocalCopyMachineEventTypes.CANCEL_EVENT_EVT]: { + target: "removeWorkflowFromStorage", + }, + }, + }, + removeWorkflowFromStorage: { + invoke: { + src: "removeCopyFromStorage", + onDone: { + target: "finish", + actions: "cleanLocalChanges", + }, + }, + }, + finish: { + type: "final", + data: ({ lastStoredVersion }, event) => { + return { + workflow: lastStoredVersion, + // @ts-ignore + isLocalStorageEmpty: _isEmpty(event?.data), + }; + }, + }, + }, + }, + { + services: services as any, + actions: actions as any, + }, +); diff --git a/ui-next/src/pages/definition/ConfirmLocalCopyDialog/state/service.ts b/ui-next/src/pages/definition/ConfirmLocalCopyDialog/state/service.ts new file mode 100644 index 0000000000..86d3599a26 --- /dev/null +++ b/ui-next/src/pages/definition/ConfirmLocalCopyDialog/state/service.ts @@ -0,0 +1,71 @@ +import fastDeepEqual from "fast-deep-equal"; +import _isNil from "lodash/isNil"; +import { + extractKeyFromContext, + removeCopyFromStorage, +} from "pages/runWorkflow/runWorkflowUtils"; +import { WorkflowDef } from "types/WorkflowDef"; +import { logger } from "utils"; + +export { removeCopyFromStorage }; + +const recoverVersionIfWorthIt = ( + wfKey: string, + savedVersion: string, + currentWf: { updateTime: number }, +): Promise | null> => { + try { + logger.log("Recovered version from Local Storage ", wfKey); + const savedVersionDef = JSON.parse(savedVersion); + + const isJsonEqual = fastDeepEqual(savedVersionDef, currentWf); + if (!isJsonEqual) { + return Promise.resolve(savedVersionDef); + } + } catch { + logger.log("Version is not parsable", wfKey); + } + + logger.log("Version is not relevant removing", wfKey); + localStorage.removeItem(wfKey); + return Promise.reject(null); +}; + +const isNewWorkflowWorthIt = ( + wfKey: string, + savedVersion: string, + currentWf: WorkflowDef, +): Promise | null> => { + try { + const savedVersionDef = JSON.parse(savedVersion); + const { name: _savedVersionName, ...restOfSavedVersion } = savedVersionDef; + + const { name: _currentVersionName, ...restOfCurrentVersion } = currentWf; + const isJsonEqual = fastDeepEqual(restOfCurrentVersion, restOfSavedVersion); + logger.log("Fast Deep Equals says json is Equal ", isJsonEqual); + if (!isJsonEqual) { + return Promise.resolve(savedVersionDef); + } + } catch { + logger.log("Could not parse the saved json."); + } + + logger.log("Discarding localStorage version"); + localStorage.removeItem(wfKey); + return Promise.reject(null); +}; + +export const consumeCopyFromLocalStorage = ( + context: any, +): Promise | null> => { + const { currentWf, isNewWorkflow } = context; + const wfKey = extractKeyFromContext(context); + const savedVersion = localStorage.getItem(wfKey); + if (!_isNil(savedVersion)) { + return isNewWorkflow + ? isNewWorkflowWorthIt(wfKey, savedVersion, currentWf) + : recoverVersionIfWorthIt(wfKey, savedVersion, currentWf); + } + + return Promise.reject(null); +}; diff --git a/ui-next/src/pages/definition/ConfirmLocalCopyDialog/state/types.ts b/ui-next/src/pages/definition/ConfirmLocalCopyDialog/state/types.ts new file mode 100644 index 0000000000..b81f87c3db --- /dev/null +++ b/ui-next/src/pages/definition/ConfirmLocalCopyDialog/state/types.ts @@ -0,0 +1,54 @@ +import { WorkflowDef } from "types/WorkflowDef"; +import { DoneInvokeEvent } from "xstate"; +import { WorkflowWithNoErrorsEvent } from "../../errorInspector/state"; + +export enum LocalCopyMachineEventTypes { + USE_LOCAL_CHANGES_EVT = "USE_LOCAL_CHANGES_EVT", + CANCEL_EVENT_EVT = "CANCEL_EVENT_EVT", + USE_LOCAL_COPY_WORKFLOW = "USE_LOCAL_COPY_WORKFLOW", + REMOVE_LOCAL_COPY = "REMOVE_LOCAL_COPY", + REMOVE_LOCAL_COPY_MESSAGE = "REMOVE_LOCAL_COPY_MESSAGE", + UPDATE_ATTRIBS_EVT = "updateAttributes", +} + +export interface LocalCopyMachineContext { + lastStoredVersion?: Partial; + workflowName: string; + currentVersion?: number; + isNewWorkflow: boolean; + currentWf: Partial; +} + +export type UseLocalChangesEvent = { + type: LocalCopyMachineEventTypes.USE_LOCAL_CHANGES_EVT; +}; + +export type CancelEvent = { + type: LocalCopyMachineEventTypes.CANCEL_EVENT_EVT; +}; + +export type RemoveLocalCopyEvent = { + type: LocalCopyMachineEventTypes.REMOVE_LOCAL_COPY; +}; + +export type UseLocalCopyChangesEvent = { + type: LocalCopyMachineEventTypes.USE_LOCAL_COPY_WORKFLOW; + workflow: Partial; +}; + +export type RemoveLocalCopyMessageEvent = { + type: LocalCopyMachineEventTypes.REMOVE_LOCAL_COPY_MESSAGE; +}; + +export type UpdateAttribsEvent = { + type: LocalCopyMachineEventTypes.UPDATE_ATTRIBS_EVT; +}; + +export type LocalCopyMachineEvents = + | UpdateAttribsEvent + | UseLocalChangesEvent + | WorkflowWithNoErrorsEvent + | RemoveLocalCopyEvent + | RemoveLocalCopyMessageEvent + | CancelEvent + | DoneInvokeEvent; diff --git a/ui-next/src/pages/definition/EditorPanel/AssistantPanel.tsx b/ui-next/src/pages/definition/EditorPanel/AssistantPanel.tsx new file mode 100644 index 0000000000..78aeae5fb4 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/AssistantPanel.tsx @@ -0,0 +1,129 @@ +import { Box } from "@mui/material"; +import Agent from "components/agent/Agent"; +import { AgentDisplayMode } from "components/agent/agent-types"; +import React, { useEffect, useRef } from "react"; +import { ActorRef } from "xstate"; +import { WorkflowDefinitionEvents } from "../state/types"; +import { AssistantPanelHeader } from "./AssistantPanelHeader"; + +export const SHRINKED_HEIGHT = 430; + +interface AssistantPanelProps { + isAgentExpanded: boolean; + agentPanelHeight: number | null; + tabsHeight: number; + errorInspectorActor: any; + definitionActor: ActorRef; + onHeaderMouseDown: (e: React.MouseEvent) => void; + onHeaderClick: (e: React.MouseEvent) => void; + onToggleExpanded: () => void; + onMaximize: () => void; + onHeightChange: (height: number) => void; + isResizing: boolean; +} + +export const AssistantPanel = ({ + isAgentExpanded, + agentPanelHeight, + tabsHeight, + errorInspectorActor, + definitionActor, + onHeaderMouseDown, + onHeaderClick, + onToggleExpanded, + onMaximize, + onHeightChange, + isResizing, +}: AssistantPanelProps) => { + const agentPanelRef = useRef(null); + + // Handle clicks outside the assistant panel to resize it to SHRINKED_HEIGHT (currently 430px) + useEffect(() => { + if (!isAgentExpanded) return; + + const handleClickOutside = (event: MouseEvent) => { + if ( + agentPanelRef.current && + !agentPanelRef.current.contains(event.target as Node) && + !isResizing + ) { + onHeightChange(SHRINKED_HEIGHT); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [isAgentExpanded, isResizing, onHeightChange]); + + return ( + + + {isAgentExpanded && ( + + + + )} + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/AssistantPanelHeader.tsx b/ui-next/src/pages/definition/EditorPanel/AssistantPanelHeader.tsx new file mode 100644 index 0000000000..ed09493501 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/AssistantPanelHeader.tsx @@ -0,0 +1,205 @@ +import AutoAwesomeIcon from "@mui/icons-material/AutoAwesome"; +import Forum from "@mui/icons-material/Forum"; +import UnfoldMore from "@mui/icons-material/UnfoldMore"; +import { Box, Button } from "@mui/material"; +import { CaretUp, NotePencilIcon } from "@phosphor-icons/react"; +import IconButton from "components/MuiIconButton"; +import Puller from "components/Puller"; +import { AgentContentTab } from "components/agent/agent-types"; +import { useAtom } from "jotai"; +import React from "react"; +import { agentContentTabAtom } from "shared/agent/agentAtomsStore"; +import { ActorRef } from "xstate"; +import { + DefinitionMachineEventTypes, + WorkflowDefinitionEvents, +} from "../state/types"; + +interface AssistantPanelHeaderProps { + isAgentExpanded: boolean; + agentPanelHeight: number | null; + definitionActor: ActorRef; + onHeaderMouseDown: (e: React.MouseEvent) => void; + onHeaderClick: (e: React.MouseEvent) => void; + onToggleExpanded: () => void; + onMaximize: () => void; +} + +export const AssistantPanelHeader = ({ + isAgentExpanded, + agentPanelHeight, + definitionActor, + onHeaderMouseDown, + onHeaderClick, + onToggleExpanded, + onMaximize, +}: AssistantPanelHeaderProps) => { + const [agentContentTab, setAgentContentTab] = useAtom(agentContentTabAtom); + + return ( + + + + + + + Assistant + + + {agentContentTab === AgentContentTab.CONVERSATIONS ? ( + + ) : ( + + )} + {isAgentExpanded && agentPanelHeight !== null && ( + { + e.stopPropagation(); + onMaximize(); + }} + onMouseDown={(e) => { + e.stopPropagation(); + }} + sx={{ + marginRight: "8px", + flexShrink: 0, + minWidth: "32px", + width: "32px", + height: "32px", + }} + title="Expand to full height" + > + + + )} + { + e.stopPropagation(); + onToggleExpanded(); + }} + sx={{ + marginRight: "8px", + }} + > + + + + + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/CodeEditorTab/CodeTab.tsx b/ui-next/src/pages/definition/EditorPanel/CodeEditorTab/CodeTab.tsx new file mode 100644 index 0000000000..4363699454 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/CodeEditorTab/CodeTab.tsx @@ -0,0 +1,286 @@ +import Editor, { Monaco } from "@monaco-editor/react"; +import { Box, IconButton, Tooltip } from "@mui/material"; +import CopyIcon from "components/v1/icons/CopyIcon"; +import _isNil from "lodash/isNil"; +import { + FunctionComponent, + useCallback, + useContext, + useEffect, + useRef, + useState, +} from "react"; +import { defaultEditorOptions } from "shared/editor"; +import { ColorModeContext } from "theme/material/ColorModeContext"; +import { colors } from "theme/tokens/variables"; +import { + configureMonaco, + JSON_FILE_NAME, +} from "utils/monacoUtils/CodeEditorUtils"; +import { ActorRef } from "xstate"; +import { ConfirmSaveDiffEditor } from "../../confirmSave"; +import { SaveWorkflowEvents } from "../../confirmSave/state/types"; +import "./MonacoDefinitionOverrides.scss"; +import { useCodeTabActor } from "./state/hook"; +import { CodeMachineEvents } from "./state/types"; + +const editorState = { + editorOptions: { + ...defaultEditorOptions, + selectOnLineNumbers: true, + }, +}; + +interface ConfirmSaveEditorWithActorProps { + saveChangesActor: ActorRef; + editorTheme: "vs-dark" | "vs-light"; +} + +const ConfirmSaveEditorWithActor: FunctionComponent< + ConfirmSaveEditorWithActorProps +> = ({ saveChangesActor, editorTheme }) => ( + +); + +interface CodeTabWithActorProps { + codeTabActor: ActorRef; + editorTheme: "vs-dark" | "vs-light"; +} + +const CodeTabWithActor: FunctionComponent = ({ + codeTabActor, + editorTheme, +}) => { + const monacoObjects = useRef(null); + const [ + { editorChanges, referenceText, shouldTakeToFirstError }, + { handleEditChanges }, + ] = useCodeTabActor(codeTabActor); + + useEffect(() => { + //Listens to state change. on State change marks the error + const editor: Monaco = monacoObjects.current; + if (shouldTakeToFirstError && editor) { + editor.trigger("keyboard", "editor.action.marker.next", {}); + } + }, [shouldTakeToFirstError, monacoObjects]); + + const highlightTextReference = useCallback(() => { + const editor: Monaco = monacoObjects.current; + + if (_isNil(editor) || _isNil(referenceText)) return; + + editor.focus(); + const matches = editor + .getModel() + .findMatches( + referenceText?.textReference, + true, + false, + false, + null, + true, + ); + if (matches) { + const match = matches[0]; + if (match) { + editor.setPosition({ + column: match.range.startColumn, + lineNumber: match.range.startLineNumber, + }); + + editor.revealLineInCenter(match.range.startLineNumber); + + const { linesDecorationsClassName, inlineClassName } = + referenceText?.referenceReason === "error" + ? { + linesDecorationsClassName: "ErrorRefStringLineDecoration", + inlineClassName: "ErrorRefStringInLineDecoration", + } + : { + linesDecorationsClassName: "TaskNameLineDecoration", + inlineClassName: "TaskNameInlineDecoration", + }; + + editor.deltaDecorations( + [], + [ + { + range: match.range, + options: { + isWholeLine: true, + linesDecorationsClassName, + }, + }, + { + range: match.range, + options: { inlineClassName }, + }, + ], + ); + } + } + }, [referenceText]); + + const handleEditorWillMount = useCallback((monaco: Monaco) => { + configureMonaco(monaco); + monaco.editor.defineTheme("vs-light", { + base: "vs", + inherit: true, + rules: [ + { + token: "number", + foreground: colors.primaryGreen, + }, + ], + colors: {}, + }); + }, []); + + const editorDidMount = useCallback( + (editor: Monaco) => { + monacoObjects.current = editor; + highlightTextReference(); + }, + [monacoObjects, highlightTextReference], + ); + + // Props to MonacoEditor + useEffect(() => { + if (monacoObjects.current && referenceText) { + highlightTextReference(); + } + }, [highlightTextReference, referenceText]); + + const [copied, setCopied] = useState(false); + + const handleCopy = useCallback(async () => { + if (editorChanges) { + try { + await navigator.clipboard.writeText(editorChanges); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + console.error("Failed to copy text:", err); + } + } + }, [editorChanges]); + + return ( + + + + + + + + + + .monaco-editor-background": { + borderRadius: 2, + }, + "& .monaco-editor .monaco-scrollable-element": { + borderRadius: 2, + }, + "& > section": { + borderRadius: 2, + }, + }} + > + { + if (typeof maybeText === "string") { + handleEditChanges!(maybeText); + } + }} + path={JSON_FILE_NAME} + /> + + + + ); +}; + +export interface CodeTabProps { + codeTabActor?: ActorRef; + saveChangesActor?: ActorRef; +} +export const CodeTab: FunctionComponent = ({ + codeTabActor, + saveChangesActor, +}) => { + const { mode } = useContext(ColorModeContext); + const editorTheme = mode === "dark" ? "vs-dark" : "vs-light"; + return ( + + + {codeTabActor && ( + + )} + {saveChangesActor && ( + + )} + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/CodeEditorTab/MonacoDefinitionOverrides.scss b/ui-next/src/pages/definition/EditorPanel/CodeEditorTab/MonacoDefinitionOverrides.scss new file mode 100644 index 0000000000..f8adcda1a8 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/CodeEditorTab/MonacoDefinitionOverrides.scss @@ -0,0 +1,23 @@ +.TaskNameInlineDecoration { + background: yellow; + cursor: pointer; + font-weight: bold; +} + +.TaskNameLineDecoration { + background: lightblue; + width: 5px !important; + margin-left: 3px; +} + +.ErrorRefStringInLineDecoration { + background: #fbb4c6; + cursor: pointer; + font-weight: bold; +} + +.ErrorRefStringLineDecoration { + background: lightblue; + width: 5px !important; + margin-left: 3px; +} diff --git a/ui-next/src/pages/definition/EditorPanel/CodeEditorTab/index.ts b/ui-next/src/pages/definition/EditorPanel/CodeEditorTab/index.ts new file mode 100644 index 0000000000..f0009d67c2 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/CodeEditorTab/index.ts @@ -0,0 +1 @@ +export * from "./CodeTab"; diff --git a/ui-next/src/pages/definition/EditorPanel/CodeEditorTab/state/actions.ts b/ui-next/src/pages/definition/EditorPanel/CodeEditorTab/state/actions.ts new file mode 100644 index 0000000000..7ea47c455d --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/CodeEditorTab/state/actions.ts @@ -0,0 +1,41 @@ +import { assign, send } from "xstate"; +import { + CodeMachineContext, + EditEvent, + DebounceEditEvent, + CodeMachineEventTypes, + HighlightTextReferenceEvent, +} from "./types"; +import { ErrorInspectorEventTypes } from "pages/definition/errorInspector/state/types"; +import { cancel } from "xstate/lib/actions"; + +export const editChanges = assign({ + editorChanges: (context, { changes }) => changes, +}); + +export const persistReferenceText = assign< + CodeMachineContext, + HighlightTextReferenceEvent +>((context, event) => { + return { + referenceText: event.reference, + }; +}); + +export const debounceEditEvent = send( + (__, { changes }) => ({ + type: CodeMachineEventTypes.EDIT_EVT, + changes, + }), + { delay: 100, id: "debounce_edit_event" }, +); + +export const checkForErrorsInWorkflow = send( + ({ editorChanges }) => ({ + type: ErrorInspectorEventTypes.VALIDATE_WORKFLOW_STRING, + workflowChanges: editorChanges, + }), + { to: ({ errorInspectorMachine }) => errorInspectorMachine! }, +); + +export const cancelDebounceEditChanges = cancel("debounce_edit_event"); diff --git a/ui-next/src/pages/definition/EditorPanel/CodeEditorTab/state/hook.ts b/ui-next/src/pages/definition/EditorPanel/CodeEditorTab/state/hook.ts new file mode 100644 index 0000000000..3766aeeca7 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/CodeEditorTab/state/hook.ts @@ -0,0 +1,32 @@ +import { ActorRef, State } from "xstate"; +import { useSelector } from "@xstate/react"; +import { + CodeMachineEvents, + CodeMachineEventTypes, + CodeMachineContext, +} from "./types"; + +export const useCodeTabActor = (actor: ActorRef) => { + const handleEditChanges = (changes: string) => { + actor.send({ + type: CodeMachineEventTypes.EDIT_EVT, + changes, + }); + }; + return [ + { + editorChanges: useSelector(actor, (state) => state.context.editorChanges), + referenceText: useSelector( + actor, + (state: State) => state.context.referenceText, + ), + shouldTakeToFirstError: useSelector( + actor, + (state: State) => state.hasTag("showFirstError"), + ), + }, + { + handleEditChanges, + }, + ] as const; +}; diff --git a/ui-next/src/pages/definition/EditorPanel/CodeEditorTab/state/index.ts b/ui-next/src/pages/definition/EditorPanel/CodeEditorTab/state/index.ts new file mode 100644 index 0000000000..79fd82215f --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/CodeEditorTab/state/index.ts @@ -0,0 +1,2 @@ +export * from "./machine"; +export * from "./types"; diff --git a/ui-next/src/pages/definition/EditorPanel/CodeEditorTab/state/machine.ts b/ui-next/src/pages/definition/EditorPanel/CodeEditorTab/state/machine.ts new file mode 100644 index 0000000000..f12b028cf6 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/CodeEditorTab/state/machine.ts @@ -0,0 +1,55 @@ +import { createMachine } from "xstate"; +import * as actions from "./actions"; +import { + CodeMachineContext, + CodeMachineEventTypes, + CodeMachineEvents, +} from "./types"; + +export const codeMachine = createMachine( + { + predictableActionArguments: true, + id: "codeDefinitionMachine", + initial: "editor", + context: { + originalWorkflow: {}, + editorChanges: "", + errorInspectorMachine: undefined, + tabRequest: undefined, + referenceText: undefined, + }, + states: { + editor: { + on: { + [CodeMachineEventTypes.EDIT_EVT]: { + actions: ["editChanges", "checkForErrorsInWorkflow"], + }, + [CodeMachineEventTypes.EDIT_DEBOUNCE_EVT]: { + actions: ["cancelDebounceEditChanges", "debounceEditEvent"], + }, + [CodeMachineEventTypes.HIGHLIGHT_TEXT_REFERENCE]: { + actions: ["persistReferenceText"], + }, + [CodeMachineEventTypes.JUMP_TO_FIRST_ERROR]: { + target: ".showFirstError", + }, + }, + initial: "idle", + states: { + idle: {}, + showFirstError: { + tags: ["showFirstError"], + after: { + 1000: { + target: "idle", + }, + }, + }, + }, + }, + }, + }, + { + actions: actions as any, + }, +); diff --git a/ui-next/src/pages/definition/EditorPanel/CodeEditorTab/state/types.ts b/ui-next/src/pages/definition/EditorPanel/CodeEditorTab/state/types.ts new file mode 100644 index 0000000000..063ce1746f --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/CodeEditorTab/state/types.ts @@ -0,0 +1,52 @@ +import { ActorRef } from "xstate"; +import { WorkflowDef } from "types"; +import { + ErrorInspectorMachineEvents, + WorkflowWithNoErrorsEvent, +} from "pages/definition/errorInspector/state/types"; + +export type CodeTextReference = { + textReference: string; + referenceReason: "error" | "info"; +}; + +export interface CodeMachineContext { + originalWorkflow: Partial; + editorChanges: string; + errorInspectorMachine?: ActorRef; + tabRequest?: number; + referenceText?: CodeTextReference; +} + +export enum CodeMachineEventTypes { + EDIT_EVT = "EDIT_EVT", + EDIT_DEBOUNCE_EVT = "EDIT_DEBOUNCE_EVT", + HIGHLIGHT_TEXT_REFERENCE = "HIGHLIGHT_TEXT_REFERENCE", + JUMP_TO_FIRST_ERROR = "JUMP_TO_FIRST_ERROR", +} + +export type EditEvent = { + type: CodeMachineEventTypes.EDIT_EVT; + changes: string; +}; + +export type HighlightTextReferenceEvent = { + type: CodeMachineEventTypes.HIGHLIGHT_TEXT_REFERENCE; + reference: CodeTextReference; +}; + +export type DebounceEditEvent = { + type: CodeMachineEventTypes.EDIT_DEBOUNCE_EVT; + changes: string; +}; + +export type JumpToFirstErrorEvent = { + type: CodeMachineEventTypes.JUMP_TO_FIRST_ERROR; +}; + +export type CodeMachineEvents = + | EditEvent + | DebounceEditEvent + | WorkflowWithNoErrorsEvent + | HighlightTextReferenceEvent + | JumpToFirstErrorEvent; diff --git a/ui-next/src/pages/definition/EditorPanel/ConfirmationDialogs.tsx b/ui-next/src/pages/definition/EditorPanel/ConfirmationDialogs.tsx new file mode 100644 index 0000000000..901b6f8f51 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/ConfirmationDialogs.tsx @@ -0,0 +1,66 @@ +import ConfirmChoiceDialog from "components/ConfirmChoiceDialog"; +import { ActorRef } from "xstate"; +import { ConfirmDialog } from "../ConfirmDialog"; +import { ConfirmLocalCopyDialog } from "../ConfirmLocalCopyDialog/ConfirmLocalCopyDialog"; +import { ConfirmWorkflowOverride } from "../confirmSave"; + +interface ConfirmationDialogsProps { + isConfirmReset: boolean; + isConfirmDelete: boolean; + isConfirmingForkRemoval: boolean; + isSaveRequest: boolean; + localCopyActor: ActorRef | undefined; + saveChangesActor: ActorRef | undefined; + onResetConfirmation: (val: boolean) => void; + onDeleteConfirmation: (val: boolean) => void; + onCancelRequest: () => void; + onConfirmLastForkRemovalRequest: () => void; +} + +export const ConfirmationDialogs = ({ + isConfirmReset, + isConfirmDelete, + isConfirmingForkRemoval, + isSaveRequest, + localCopyActor, + saveChangesActor, + onResetConfirmation, + onDeleteConfirmation, + onCancelRequest, + onConfirmLastForkRemovalRequest, +}: ConfirmationDialogsProps) => { + return ( + <> + {isConfirmReset && ( + + )} + {isConfirmDelete && ( + + )} + {isConfirmingForkRemoval && ( + + )} + {localCopyActor && ( + + )} + {isSaveRequest && saveChangesActor && ( + + )} + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/CustomTooltip.tsx b/ui-next/src/pages/definition/EditorPanel/CustomTooltip.tsx new file mode 100644 index 0000000000..c38a99e4a5 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/CustomTooltip.tsx @@ -0,0 +1,104 @@ +import React from "react"; +import { createPortal } from "react-dom"; +import { + Popper, + Paper, + ClickAwayListener, + PopperPlacementType, +} from "@mui/material"; +import { styled } from "@mui/material/styles"; +import MuiIconButton from "components/MuiIconButton"; +import XCloseIcon from "components/v1/icons/XCloseIcon"; + +interface CustomTooltipProps { + open: boolean; + anchorEl: HTMLElement | null; + onClose: () => void; + content: React.ReactNode; + placement?: PopperPlacementType; + maxWidth?: number; +} + +const StyledPaper = styled(Paper)(({ theme }) => ({ + padding: theme.spacing(2), + backgroundColor: "#F4F9FE", + borderRadius: theme.spacing(1), + boxShadow: "0px 2px 8px rgba(0, 0, 0, 0.15)", + border: "1px solid #0D94DB", + position: "relative", + marginTop: theme.spacing(6), + "&::before": { + content: '""', + position: "absolute", + top: -5, + left: 20, + width: 10, + height: 10, + background: "white", + border: "1px solid #0D94DB", + backgroundColor: "#F4F9FE", + transform: "rotate(45deg)", + borderRight: "none", + borderBottom: "none", + }, +})); + +const CustomTooltip: React.FC = ({ + open, + anchorEl, + onClose, + content, + placement = "bottom-start", + maxWidth = 400, +}) => { + return ( + <> + {open && + createPortal( +
    , + document.body, + )} + + + + + + + {content} + + + + + ); +}; + +export default CustomTooltip; diff --git a/ui-next/src/pages/definition/EditorPanel/DependenciesTab/DependenciesTab.tsx b/ui-next/src/pages/definition/EditorPanel/DependenciesTab/DependenciesTab.tsx new file mode 100644 index 0000000000..b8abacb6ed --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/DependenciesTab/DependenciesTab.tsx @@ -0,0 +1,73 @@ +import { Box, Paper, Typography } from "@mui/material"; +import { useSelector } from "@xstate/react"; +import { WorkflowEditContext } from "pages/definition/state"; +import { pluginRegistry } from "plugins/registry"; +import { WorkflowDependencies } from "plugins/registry/types"; +import { useContext, useMemo } from "react"; +import { scanTasksForDependenciesInWorkflow } from "utils/workflow"; +import TaskFormSection from "../TaskFormTab/forms/TaskFormSection"; + +const DependenciesTab = () => { + const { workflowDefinitionActor } = useContext(WorkflowEditContext); + const workflowChanges = useSelector( + workflowDefinitionActor!, + (state) => state.context.workflowChanges, + ); + + // Extract all dependencies from the workflow + const rawDependencies = useMemo( + () => scanTasksForDependenciesInWorkflow(workflowChanges), + [workflowChanges], + ); + + // Map to the plugin interface shape + const dependencies: WorkflowDependencies = useMemo( + () => ({ + integrationNames: rawDependencies.integrationNames || [], + promptNames: rawDependencies.promptNames || [], + userFormsNameVersion: rawDependencies.userFormsNameVersion || [], + schemas: rawDependencies.schemas || [], + secrets: rawDependencies.secrets || [], + env: rawDependencies.env || [], + workflowName: rawDependencies.workflowName, + workflowVersion: rawDependencies.workflowVersion, + }), + [rawDependencies], + ); + + // Get dependency sections from plugins + const sections = pluginRegistry.getDependencySections(); + + if (sections.length === 0) { + return ( + + + No dependency sections available. + + + ); + } + + return ( + + theme.palette.customBackground.form, + }} + > + {sections.map((section) => { + const SectionComponent = section.component; + return ( + + + + ); + })} + + + ); +}; + +export default DependenciesTab; diff --git a/ui-next/src/pages/definition/EditorPanel/EditorPanel.tsx b/ui-next/src/pages/definition/EditorPanel/EditorPanel.tsx new file mode 100644 index 0000000000..0fcc06d57f --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/EditorPanel.tsx @@ -0,0 +1,491 @@ +import { Box } from "@mui/material"; +import { useSelector } from "@xstate/react"; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { colors } from "theme/tokens/variables"; +import { FEATURES, featureFlags, logger } from "utils"; +import { ActorRef, EventObject, State } from "xstate"; +import ErrorInspector from "../errorInspector/ErrorInspector"; +import { + DefinitionMachineContext, + DefinitionMachineEventTypes, + WorkflowDefinitionEvents, +} from "../state/types"; +import { AssistantPanel, SHRINKED_HEIGHT } from "./AssistantPanel"; +import { ConfirmationDialogs } from "./ConfirmationDialogs"; +import { EditorTabs } from "./EditorTabs"; +import { TabContent } from "./TabContent"; +import { useDefinitionMachine } from "./hook"; + +const agentEnabled = featureFlags.isEnabled(FEATURES.SHOW_AGENT); + +// Type helper for ActorRef with children property (exists at runtime but not in types) +type ActorRefWithChildren = ActorRef & { + children?: { + get: ( + id: string, + ) => ActorRef | undefined; + }; +}; + +interface EditorPanelProps { + definitionActor: ActorRef; +} + +const EditorPanel = ({ definitionActor }: EditorPanelProps) => { + const tabsContainerRef = useRef(null); + const [ + { + handleConfirmReset, + handleConfirmDelete, + handleCancelRequest, + changeTab, + handleConfirmLastForkRemovalRequest, + setLeftPanelExpanded, + }, + { + isConfirmDelete, + isConfirmReset, + openedTab, + isSaveRequest, + isConfirmingForkRemoval, + isRunWorkflow, + }, + ] = useDefinitionMachine(definitionActor); + + const isReady = useSelector( + definitionActor, + (state: State) => state.matches("ready"), + ); + + const isInTaskFormState = useSelector( + definitionActor, + (state: State) => + state.matches("ready.rightPanel.opened.taskEditor"), + ); + + const isFirstTimeFlowWorkflowDialog = useSelector( + definitionActor, + (state: State) => + state.hasTag("showCongratsMessage"), + ); + + const isShowRunMessageDialog = useSelector( + definitionActor, + (state: State) => state.hasTag("showRunMessage"), + ); + + const isShowDependenciesDialog = useSelector( + definitionActor, + (state: State) => + state.hasTag("showDependenciesMessage"), + ); + + const isAgentExpanded = useSelector( + definitionActor, + (state: State) => + state.context.isAgentExpanded ?? false, + ); + + const [tabsHeight, setTabsHeight] = useState(48); + const [agentPanelHeight, setAgentPanelHeight] = useState(null); + const [isResizing, setIsResizing] = useState(false); + const isResizingRef = useRef(false); + const resizeStartRef = useRef<{ x: number; y: number } | null>(null); + const intendedHeightRef = useRef(null); + const resizeStateRef = useRef<{ + startY: number; + startHeight: number; + maxHeight: number; + containerRect: DOMRect; + wasCollapsed: boolean; + hasExpanded: boolean; + } | null>(null); + const shouldHandleClickRef = useRef<{ wasCollapsed: boolean } | null>(null); + const editorPanelContainerRef = useRef(null); + + // Handle document-level mouse events during resize + // Note: We use refs (wasCollapsed) instead of XState state (isAgentExpanded) during drag + // to avoid stale state checks and unnecessary re-renders + const handleMouseMove = useCallback( + (moveEvent: MouseEvent) => { + if (!resizeStartRef.current || !resizeStateRef.current) return; + + // Check if mouse moved significantly (more than 5px) to distinguish drag from click + const moveDistance = Math.sqrt( + Math.pow(moveEvent.clientX - resizeStartRef.current.x, 2) + + Math.pow(moveEvent.clientY - resizeStartRef.current.y, 2), + ); + + if (moveDistance > 5) { + isResizingRef.current = true; + } + + if (!isResizingRef.current) return; + + const { startY, startHeight, maxHeight, wasCollapsed } = + resizeStateRef.current; + + // Calculate how much the mouse moved (positive = moved down) + const diff = moveEvent.clientY - startY; + // When dragging down, we increase height (top edge moves down, bottom stays fixed) + // When dragging up, we decrease height (top edge moves up, bottom stays fixed) + const newHeight = Math.max(200, Math.min(maxHeight, startHeight - diff)); + + // If we started from collapsed state, expand the panel only once + // Use wasCollapsed from ref (not isAgentExpanded from XState) to avoid stale checks + if (wasCollapsed && !resizeStateRef.current.hasExpanded) { + // Mark as expanded to prevent multiple expansion calls + resizeStateRef.current.hasExpanded = true; + // Store intended height in ref for immediate access + intendedHeightRef.current = newHeight; + // CRITICAL: Set height first, then expand in next tick + // This ensures the height state is set before the component re-renders with expanded=true + setAgentPanelHeight(newHeight); + // Use setTimeout to ensure height state update is processed before expansion + // This prevents the panel from briefly using calc() value (full height) + setTimeout(() => { + definitionActor.send({ + type: DefinitionMachineEventTypes.TOGGLE_AGENT_EXPANDED, + expanded: true, + }); + }, 0); + } else { + intendedHeightRef.current = newHeight; + setAgentPanelHeight(newHeight); + } + }, + [definitionActor], + ); + + const handleMouseUp = useCallback(() => { + const wasResizing = isResizingRef.current; + const wasCollapsed = resizeStateRef.current?.wasCollapsed ?? false; + isResizingRef.current = false; + setIsResizing(false); + + // If it was just a click (not a drag), mark it for handleHeaderClick to process + if (!wasResizing && resizeStateRef.current) { + shouldHandleClickRef.current = { wasCollapsed }; + } else { + shouldHandleClickRef.current = null; + } + + resizeStartRef.current = null; + resizeStateRef.current = null; + // Note: If it was a drag (wasResizing = true), the state is already updated + // via handleMouseMove, so we don't need to do anything here + // Click handling is done in handleHeaderClick + }, []); + + useEffect(() => { + if (!isResizing || !resizeStateRef.current) return; + + window.addEventListener("mousemove", handleMouseMove); + window.addEventListener("mouseup", handleMouseUp); + + return () => { + window.removeEventListener("mousemove", handleMouseMove); + window.removeEventListener("mouseup", handleMouseUp); + }; + }, [isResizing, handleMouseMove, handleMouseUp]); + + useEffect(() => { + if (!tabsContainerRef.current) return; + + const updateTabsHeight = () => { + if (tabsContainerRef.current) { + const height = tabsContainerRef.current.offsetHeight || 48; + setTabsHeight((prev) => (prev !== height ? height : prev)); + } + }; + + updateTabsHeight(); + const resizeObserver = new ResizeObserver(updateTabsHeight); + resizeObserver.observe(tabsContainerRef.current); + + return () => { + resizeObserver.disconnect(); + }; + }, []); + + const handleNextButtonClick = () => { + definitionActor.send( + DefinitionMachineEventTypes.NEXT_STEP_IMPORT_SUCCESSFUL_DIALOG, + ); + }; + + const handleDismissTutorial = () => { + definitionActor.send( + DefinitionMachineEventTypes.DISMISS_IMPORT_SUCCESSFUL_DIALOG, + ); + }; + + logger.debug("Rendering Editor Panel"); + + const handleResetConfirmation = (val: boolean) => + (val ? handleConfirmReset : handleCancelRequest)(); + + const handleDeleteWorkflowVersionConfirmation = (val: boolean) => + (val ? handleConfirmDelete : handleCancelRequest)(); + + const localCopyActor = ( + definitionActor as ActorRefWithChildren + ).children?.get("localCopyMachine"); + + const saveChangesActor = ( + definitionActor as ActorRefWithChildren + ).children?.get("saveChangesMachine"); + + const errorInspectorActor = useSelector( + definitionActor, + (state: State) => + state.context.errorInspectorMachine, + ); + + // Calculate effective height - use default when auto-expanding to prevent top positioning + const effectiveAgentPanelHeight = useMemo(() => { + if (isAgentExpanded && agentPanelHeight === null) { + // Auto-expanding without a set height - use default 430px + return SHRINKED_HEIGHT; + } + return agentPanelHeight; + }, [isAgentExpanded, agentPanelHeight]); + + // Calculate available height for tab content (accounting for error inspector and assistant panel) + const getTabContentHeight = useCallback(() => { + const errorInspectorHeight = errorInspectorActor ? 50 : 0; + let assistantPanelHeight = 0; + + if (agentEnabled) { + if (isAgentExpanded) { + assistantPanelHeight = effectiveAgentPanelHeight || 0; + } else { + assistantPanelHeight = 50; // Header height when collapsed + } + } + + const totalOffset = errorInspectorHeight + assistantPanelHeight; + return totalOffset > 0 ? `calc(100% - ${totalOffset}px)` : "100%"; + }, [isAgentExpanded, effectiveAgentPanelHeight, errorInspectorActor]); + + const handleHeaderMouseDown = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + if (!editorPanelContainerRef.current) return; + const containerRect = + editorPanelContainerRef.current.getBoundingClientRect(); + const containerHeight = containerRect.height; + const maxHeight = + containerHeight - tabsHeight - (errorInspectorActor ? 50 : 0); + + // When collapsed, start with collapsed height (50px) + // When expanded, use current height or maxHeight + const startHeight = isAgentExpanded ? agentPanelHeight || maxHeight : 50; // Collapsed height + + // If starting from collapsed, calculate initial height based on mouse position + // This prevents the panel from using calc() value when it expands + if (!isAgentExpanded && agentPanelHeight === null) { + // Calculate height based on distance from bottom of container + // Mouse Y position relative to container bottom + const mouseYFromBottom = containerRect.bottom - e.clientY; + // Initial height is the distance from bottom, clamped between min and max + const initialHeight = Math.max( + 200, + Math.min(maxHeight, mouseYFromBottom), + ); + // Set height immediately so it's available when panel expands + setAgentPanelHeight(initialHeight); + // Store resize state with collapsed height as start point + resizeStateRef.current = { + startY: e.clientY, + startHeight: 50, // Use 50px (collapsed height) as starting point for drag calculations + maxHeight, + containerRect, + wasCollapsed: true, + hasExpanded: false, + }; + } else { + // Store resize state in refs for the useEffect to use + resizeStateRef.current = { + startY: e.clientY, + startHeight, + maxHeight, + containerRect, + wasCollapsed: !isAgentExpanded, + hasExpanded: false, + }; + } + + resizeStartRef.current = { x: e.clientX, y: e.clientY }; + isResizingRef.current = false; + setIsResizing(true); + }, + [isAgentExpanded, agentPanelHeight, tabsHeight, errorInspectorActor], + ); + + const handleHeaderClick = useCallback( + (e: React.MouseEvent) => { + // Prevent the click from propagating if it was on a button + if ( + (e.target as HTMLElement).closest("button") || + (e.target as HTMLElement).closest("a") + ) { + return; + } + + // Only handle click if it was marked as a click (not a drag) in handleMouseUp + const clickInfo = shouldHandleClickRef.current; + if (!clickInfo) { + return; + } + + // Clear the ref so we don't handle this click again + shouldHandleClickRef.current = null; + + if (clickInfo.wasCollapsed) { + // If collapsed, expand to full height + if (!editorPanelContainerRef.current) return; + const containerRect = + editorPanelContainerRef.current.getBoundingClientRect(); + const containerHeight = containerRect.height; + const maxHeight = + containerHeight - tabsHeight - (errorInspectorActor ? 50 : 0); + // Set height first, then toggle + setAgentPanelHeight(maxHeight); + definitionActor.send({ + type: DefinitionMachineEventTypes.TOGGLE_AGENT_EXPANDED, + expanded: true, + }); + } else { + // If expanded, collapse + definitionActor.send({ + type: DefinitionMachineEventTypes.TOGGLE_AGENT_EXPANDED, + expanded: false, + }); + } + }, + [tabsHeight, errorInspectorActor, definitionActor], + ); + + const handleToggleExpanded = useCallback(() => { + definitionActor.send({ + type: DefinitionMachineEventTypes.TOGGLE_AGENT_EXPANDED, + }); + }, [definitionActor]); + + const handleMaximize = useCallback(() => { + if (!editorPanelContainerRef.current) return; + const containerRect = + editorPanelContainerRef.current.getBoundingClientRect(); + const containerHeight = containerRect.height; + const maxHeight = + containerHeight - tabsHeight - (errorInspectorActor ? 50 : 0); + setAgentPanelHeight(maxHeight); + }, [tabsHeight, errorInspectorActor]); + + const handleHeightChange = useCallback((height: number) => { + setAgentPanelHeight(height); + }, []); + + return ( + <> + {isResizing && ( + + )} + + theme.palette?.mode === "dark" ? colors.gray14 : undefined, + backgroundColor: (theme) => theme.palette.customBackground.form, + }} + > + + + + + + + + {errorInspectorActor && ( + + )} + + {agentEnabled && ( + + )} + + + + ); +}; + +export default EditorPanel; diff --git a/ui-next/src/pages/definition/EditorPanel/EditorTabs.tsx b/ui-next/src/pages/definition/EditorPanel/EditorTabs.tsx new file mode 100644 index 0000000000..678f7ead50 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/EditorTabs.tsx @@ -0,0 +1,319 @@ +import { Badge, Box, Button, Stack } from "@mui/material"; +import { Tab, Tabs } from "components"; +import IconButton from "components/MuiIconButton"; +import DoubleArrowRightIcon from "components/v1/icons/DoubleArrowRightIcon"; +import React, { forwardRef, useRef } from "react"; +import { FEATURES, featureFlags } from "utils"; +import { ActorRef, EventObject } from "xstate"; +import { + CODE_TAB, + DEPENDENCIES_TAB, + RUN_TAB, + TASK_TAB, + WORKFLOW_TAB, +} from "../state/constants"; +import { WorkflowDefinitionEvents } from "../state/types"; +import CustomTooltip from "./CustomTooltip"; + +const isPlayground = featureFlags.isEnabled(FEATURES.PLAYGROUND); + +// Type helper for ActorRef with children property +type ActorRefWithChildren = ActorRef & { + children?: { + get: ( + id: string, + ) => ActorRef | undefined; + }; +}; + +const WorkflowTabContent = forwardRef< + HTMLDivElement, + React.HTMLAttributes +>((props, ref) => { + return ( +
    + Workflow +
    + ); +}); + +interface EditorTabsProps { + openedTab: number; + definitionActor: ActorRef; + changeTab: (tab: number) => void; + setLeftPanelExpanded: () => void; + isFirstTimeFlowWorkflowDialog: boolean; + isShowRunMessageDialog: boolean; + isShowDependenciesDialog: boolean; + handleNextButtonClick: () => void; + handleDismissTutorial: () => void; + tabsContainerRef: React.RefObject; +} + +export const EditorTabs = ({ + openedTab, + definitionActor, + changeTab, + setLeftPanelExpanded, + isFirstTimeFlowWorkflowDialog, + isShowRunMessageDialog, + isShowDependenciesDialog, + handleNextButtonClick, + handleDismissTutorial, + tabsContainerRef, +}: EditorTabsProps) => { + const workflowTabRef = useRef(null); + const runTabRef = useRef(null); + const dependenciesTabRef = useRef(null); + + return ( + + + + + + + } + onClick={() => changeTab(WORKFLOW_TAB)} + disabled={ + ( + definitionActor as ActorRefWithChildren + ).children?.get("flowMachine") == null + } + /> + changeTab(TASK_TAB)} + disabled={ + ( + definitionActor as ActorRefWithChildren + ).children?.get("flowMachine") == null + } + /> + changeTab(CODE_TAB)} + /> + Run
    } + onClick={() => changeTab(RUN_TAB)} + /> + {isPlayground ? ( + + + Dependencies + + + + } + onClick={() => changeTab(DEPENDENCIES_TAB)} + /> + ) : null} + + + {workflowTabRef.current && ( + + + + Congratulations! Your first workflow is ready to use. + + + 🎉 + + + + You can define inputs and outputs for your workflow or add an + optional JSON schema verification. Discover more features down + the form! + + + + + + } + placement="bottom-start" + /> + )} + + {runTabRef.current && ( + + + + Before you execute + + + 🚀 + + + + You can test your workflow with different arguments by editing + the Input params. + + + Pro Tip: Idempotency key is a unique, + user-generated key to prevent duplicate executions. + + + + + + } + placement="bottom-start" + /> + )} + {dependenciesTabRef.current && ( + + + + Before you execute + + + 🔗 + + + + Your workflow depends on integrations and models. Before + executing, make sure to configure them correctly. + + + You can add dependencies to your workflow by going to the + integration menu + + + + + + } + placement="bottom-start" + /> + )} + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/HeadActionButtons.tsx b/ui-next/src/pages/definition/EditorPanel/HeadActionButtons.tsx new file mode 100644 index 0000000000..492d8f4723 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/HeadActionButtons.tsx @@ -0,0 +1,259 @@ +import Stack from "@mui/material/Stack"; +import _isEmpty from "lodash/isEmpty"; +import { FunctionComponent, useState } from "react"; +import { ActorRef } from "xstate"; +import { Key } from "ts-key-enum"; +import { useHotkeys } from "react-hotkeys-hook"; +import _debounce from "lodash/debounce"; + +import { ButtonTooltip, ButtonTooltipProps } from "components/ButtonTooltip"; +import DownloadIcon from "components/v1/icons/DownloadIcon"; +import ResetIcon from "components/v1/icons/ResetIcon"; +import SaveIcon from "components/v1/icons/SaveIcon"; +import TrashIcon from "components/v1/icons/TrashIcon"; + +import { exportObjToFile } from "utils"; +import { + DefinitionMachineEventTypes, + WorkflowDefinitionEvents, +} from "../state/types"; +import { useWorkflowChanges } from "../state/useMadeChanges"; +import SplitButton from "components/v1/ConductorSplitButton"; +import { SnackbarMessage } from "components/SnackbarMessage"; +import { HOT_KEYS_WORKFLOW_DEFINITION } from "utils/constants/common"; +import { UnderlinedText } from "components/v1/UnderlinedText"; +import { useAuth } from "shared/auth"; +import { RunWorkflowButton } from "./RunWorkflowButton"; + +export interface HeaderActionButtonsProps { + definitionActor: ActorRef; +} +export const HeadActionButtons: FunctionComponent = ({ + definitionActor: service, +}) => { + const { isTrialExpired } = useAuth(); + const [errorMessage, setErrorMessage] = useState(null); + + const handleSaveRequest = () => + service.send({ type: DefinitionMachineEventTypes.SAVE_EVT }); + + const handleSaveAsNewVersionRequest = () => { + service.send({ + type: DefinitionMachineEventTypes.SAVE_EVT, + isNewVersion: true, + }); + }; + const handleResetRequest = () => + service.send({ type: DefinitionMachineEventTypes.RESET_EVT }); + + const handleDeleteRequest = () => + service.send({ type: DefinitionMachineEventTypes.DELETE_EVT }); + + const { madeChanges, isNewWorkflow, workflowChanges } = + useWorkflowChanges(service); + + const emptyTaskList = _isEmpty(workflowChanges?.tasks); + + const handleDownloadFile = () => { + exportObjToFile({ + data: workflowChanges, + fileName: `${workflowChanges.name || "new"}_${ + workflowChanges.version + }.json`, + type: `application/json`, + }); + }; + + const handleSaveAndRunRequest = () => { + service.send({ type: DefinitionMachineEventTypes.HANDLE_SAVE_AND_RUN }); + }; + + const handleSaveAndCreateNewRequest = () => { + service.send({ + type: DefinitionMachineEventTypes.HANDLE_SAVE_AND_CREATE_NEW, + }); + }; + + const buttons: ButtonTooltipProps[] = [ + { + id: "head-action-delete-btn", + variant: "text", + tooltip: + "Delete this version of the workflow definition, previous executions will not be remove (Ctrl D)", + disabled: isNewWorkflow || isTrialExpired, + onClick: handleDeleteRequest, + "data-testid": "workflow-definition-delete-button", + sx: { color: (theme) => theme.palette.error.main }, + startIcon: , + children: , + }, + { + id: "head-action-reset-btn", + variant: "text", + tooltip: "Reset the editor content to the last saved version (Ctrl R)", + disabled: !madeChanges || emptyTaskList, + onClick: handleResetRequest, + "data-testid": "workflow-definition-reset-button", + startIcon: , + children: , + sx: { color: (theme) => theme.palette.error.main }, + }, + { + id: "head-action-download-btn", + variant: "text", + tooltip: "Download JSON as file (Ctrl W)", + disabled: false, + onClick: handleDownloadFile, + "data-testid": "workflow-definition-download-button", + startIcon: , + children: , + }, + ]; + + const saveSplitButtonOptions = [ + // { + // label: , + // id: "save-and-run-btn", + // onClick: handleSaveAndRunRequest, + // }, + { + label: ( + + ), + id: "save-and-create-new-btn", + onClick: handleSaveAndCreateNewRequest, + }, + ]; + + if (!isNewWorkflow) { + saveSplitButtonOptions.push({ + label: ( + + ), + id: "save-as-new-version-btn", + onClick: handleSaveAsNewVersionRequest, + }); + } + + const debounceSaveRequest = _debounce(handleSaveRequest, 500); + + // Hotkeys for save workflow + useHotkeys( + [ + `${Key.Control} + R`, + `${Key.Control} + S`, + `${Key.Control} + E`, + `${Key.Control} + S + N`, + `${Key.Control} + W`, + `${Key.Control} + D`, + `${Key.Control} + S + C`, + ], + (keyboardEvent, { keys }) => { + keyboardEvent.preventDefault(); + const joinedKeys = keys?.join(); + + switch (joinedKeys) { + // 1. Save + case [Key.Control, "S"].join().toLowerCase(): { + if (madeChanges && !emptyTaskList && !isTrialExpired) { + debounceSaveRequest(); + } + break; + } + + // 2. Save & Run + case [Key.Control, "E"].join().toLowerCase(): { + if (!emptyTaskList && !isTrialExpired) { + debounceSaveRequest.cancel(); + handleSaveAndRunRequest(); + } + break; + } + + // 3. Save as new version + case [Key.Control, "S", "N"].join().toLowerCase(): { + debounceSaveRequest.cancel(); + + if ( + !isNewWorkflow && + madeChanges && + !emptyTaskList && + !isTrialExpired + ) { + handleSaveAsNewVersionRequest(); + } + + break; + } + + // 4. Reset + case [Key.Control, "R"].join().toLowerCase(): { + if (madeChanges && !emptyTaskList) { + handleResetRequest(); + } + + break; + } + + // 5. Download workflow definition JSON + case [Key.Control, "W"].join().toLowerCase(): { + handleDownloadFile(); + break; + } + + // 6. Delete workflow definition + case [Key.Control, "D"].join().toLowerCase(): { + if (!isNewWorkflow && !isTrialExpired) { + handleDeleteRequest(); + } + + break; + } + + // 7. Save & Create New + case [Key.Control, "S", "C"].join().toLowerCase(): { + debounceSaveRequest.cancel(); + if (madeChanges && !emptyTaskList && !isTrialExpired) { + handleSaveAndCreateNewRequest(); + } + break; + } + } + }, + { + scopes: HOT_KEYS_WORKFLOW_DEFINITION, + enableOnFormTags: ["INPUT", "TEXTAREA", "SELECT"], + }, + ); + + return ( + + {errorMessage && ( + { + setErrorMessage(""); + }} + severity="error" + message={errorMessage} + /> + )} + {buttons.map(({ id, ...props }) => ( + + ))} + + + + } + disabled={!madeChanges || emptyTaskList || isTrialExpired} + id={"head-action-save-btn"} + options={saveSplitButtonOptions} + primaryOnClick={handleSaveRequest} + tooltip="Save this definition (Ctrl S)" + data-testid="workflow-definition-save-button" + > + + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/RunWorkflowButton.tsx b/ui-next/src/pages/definition/EditorPanel/RunWorkflowButton.tsx new file mode 100644 index 0000000000..adcc974cc5 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/RunWorkflowButton.tsx @@ -0,0 +1,34 @@ +import { FunctionComponent } from "react"; +import { ActorRef } from "xstate"; +import { + DefinitionMachineEventTypes, + WorkflowDefinitionEvents, +} from "../state/types"; +import { ButtonTooltip } from "components/ButtonTooltip"; +import RocketLaunchIcon from "@mui/icons-material/RocketLaunch"; +import { UnderlinedText } from "components/v1/UnderlinedText"; + +export interface RunWorkflowButtonProps { + definitionActor: ActorRef; + disabled: boolean; +} + +export const RunWorkflowButton: FunctionComponent = ({ + definitionActor: service, + disabled, +}) => { + const executeWorkflow = () => { + service.send({ type: DefinitionMachineEventTypes.HANDLE_SAVE_AND_RUN }); + }; + return ( + } + children={} + disabled={disabled} + /> + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TabContent.tsx b/ui-next/src/pages/definition/EditorPanel/TabContent.tsx new file mode 100644 index 0000000000..93e582e24b --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TabContent.tsx @@ -0,0 +1,150 @@ +import { Box } from "@mui/material"; +import { FEATURES, featureFlags } from "utils"; +import { ActorRef, EventObject } from "xstate"; +import { RunWorkFlowForm } from "../RunWorkflow"; +import { RunMachineEvents } from "../RunWorkflow/state/types"; +import { + CODE_TAB, + DEPENDENCIES_TAB, + RUN_TAB, + TASK_TAB, + WORKFLOW_TAB, +} from "../state/constants"; +import { WorkflowDefinitionEvents } from "../state/types"; +import { WorkflowMetadataEvents } from "../WorkflowMetadata/state/types"; +import { CodeTab } from "./CodeEditorTab"; +import DependenciesTab from "./DependenciesTab/DependenciesTab"; +import { TaskForm } from "./TaskFormTab"; +import { WorkflowPropertiesForm } from "./WorkflowPropertiesFormTab"; + +const isPlayground = featureFlags.isEnabled(FEATURES.PLAYGROUND); + +// Type helper for ActorRef with children property +type ActorRefWithChildren = ActorRef & { + children?: { + get: ( + id: string, + ) => ActorRef | undefined; + }; +}; + +interface TabContentProps { + openedTab: number; + isReady: boolean; + isRunWorkflow: boolean; + isInTaskFormState: boolean; + definitionActor: ActorRef; + getTabContentHeight: () => string; +} + +export const TabContent = ({ + openedTab, + isReady, + isRunWorkflow, + isInTaskFormState, + definitionActor, + getTabContentHeight, +}: TabContentProps) => { + return ( + + {openedTab === TASK_TAB && + ( + definitionActor as ActorRefWithChildren + ).children?.get("flowMachine") != null ? ( + + + + ) : null} + + {isReady && + openedTab === WORKFLOW_TAB && + (() => { + const workflowMetadataActor = ( + definitionActor as ActorRefWithChildren + ).children?.get("workflowTabMetaEditor"); + return workflowMetadataActor != null ? ( + + + + ) : null; + })()} + + {openedTab === CODE_TAB ? ( + + + ).children?.get("codeMachine")} + saveChangesActor={( + definitionActor as ActorRefWithChildren + ).children?.get("saveChangesMachine")} + /> + + ) : null} + + {openedTab === RUN_TAB && + isRunWorkflow && + (() => { + const runTabActor = ( + definitionActor as ActorRefWithChildren + ).children?.get("runWorkflowMachine"); + return runTabActor != null ? ( + + + + ) : null; + })()} + {openedTab === DEPENDENCIES_TAB && isPlayground && ( + + + + )} + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskForm.tsx new file mode 100644 index 0000000000..f28228f22a --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskForm.tsx @@ -0,0 +1,79 @@ +import { Box, Paper } from "@mui/material"; +import { useSelector } from "@xstate/react"; +import { + TaskFormEvents, + TaskFormMachineContext, +} from "pages/definition/EditorPanel/TaskFormTab/state"; +import { WorkflowDefinitionEvents } from "pages/definition/state/types"; +import { FunctionComponent } from "react"; +import { ActorRef, State } from "xstate"; +import TaskFormContent from "./TaskFormContent"; +import { TaskFormContextProvider } from "./state"; +export interface TaskFormProps { + formTaskActor: ActorRef; +} + +const NoTaskSelected = () => ( + + No task selected + +); + +const TaskForm: FunctionComponent = ({ formTaskActor }) => { + const hasTaskToEdit = useSelector( + formTaskActor!, + (state: State) => state.matches("rendered"), + ); + + const taskType = useSelector( + formTaskActor!, + (state: State) => state.context?.originalTask?.type, + ); + + return hasTaskToEdit && taskType && formTaskActor ? ( + + + + ) : ( + + ); +}; + +const MaybeTaskForm: FunctionComponent<{ + workflowDefinitionActor: ActorRef; + isInTaskFormState: boolean; +}> = ({ workflowDefinitionActor, isInTaskFormState }) => { + const formTaskActor = + //@ts-ignore next-line + workflowDefinitionActor?.children?.get("formTaskMachine"); + + return ( + theme.palette.customBackground.form, + }} + > + {isInTaskFormState ? ( + formTaskActor && + ) : ( + + )} + + ); +}; + +export default MaybeTaskForm; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskFormContent.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskFormContent.tsx new file mode 100644 index 0000000000..66ced27b6b --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskFormContent.tsx @@ -0,0 +1,522 @@ +import { Box, Link } from "@mui/material"; +import { WarningIcon } from "@phosphor-icons/react"; +import { useActor, useSelector } from "@xstate/react"; +import ConductorTooltip from "components/conductorTooltip/ConductorTooltip"; +import theme from "components/flow/theme"; +import DocsIcon from "components/v1/icons/DocsIcon"; +import { + BusinessRuleForm, + DoWhileForm, + DynamicForkOperatorForm, + DynamicOperatorForm, + EventTaskForm, + GetSignedJwtForm, + GetWorkflowTaskForm, + HTTPPollTaskForm, + HTTPTaskForm, + INLINETaskForm, + JDBCTaskForm, + JOINTaskForm, + JSONJQTransformForm, + KafkaTaskForm, + OpsGenieTaskForm, + QueryProcessorTaskForm, + SetVariableOperatorForm, + SimpleTaskForm, + StartWorkflowTaskForm, + SubWorkflowOperatorForm, + SwitchOperatorForm, + TerminateOperatorForm, + TerminateWorkflowForm, + UnknownTaskForm, + WaitTaskForm, + YieldTaskForm, +} from "pages/definition/EditorPanel/TaskFormTab/forms"; +import TaskFormHeader from "pages/definition/EditorPanel/TaskFormTab/forms/TaskFormHeader/TaskFormHeader"; +import { FormMachineActionTypes } from "pages/definition/EditorPanel/TaskFormTab/state"; +import { WorkflowEditContext } from "pages/definition/state"; +import { pluginRegistry } from "plugins/registry"; +import { + FunctionComponent, + useCallback, + useContext, + useEffect, + useRef, + useState, +} from "react"; +import { useAuth } from "shared/auth"; +import { colors } from "theme/tokens/variables"; +import { FormTaskType, TaskDef, TaskType } from "types"; +import { updateField } from "utils/fieldHelpers"; +import { FEATURES, featureFlags } from "utils/flags"; +import { TaskStats } from "./TaskStats/TaskStats"; +import { ConductorCacheOutput } from "./forms/ConductorCacheOutputForm"; +import { MCPTaskForm } from "./forms/MCPTaskForm"; +import { MaybeVariable } from "./forms/MaybeVariable"; +import { Optional } from "./forms/OptionalFieldForm"; +import TaskFormSection from "./forms/TaskFormSection"; +import { OpenTestTaskButton } from "./forms/TestTaskButton/OpenTestTaskButton"; +import { UpdateTaskForm } from "./forms/UpdateTaskForm"; +import { TaskFormProps } from "./forms/types"; +import { TaskFormContext } from "./state"; +import { taskDescriptions } from "./taskDescription"; + +const ENABLE_TASK_STATS = featureFlags.isEnabled(FEATURES.DISABLE_TASK_STATS); + +/** + * Get the task form component for a given task type. + * First checks the plugin registry for enterprise task forms, + * then falls back to core OSS task forms. + */ +const getTaskForm = (type: string) => { + // First check plugin registry for enterprise task forms + const pluginForm = pluginRegistry.getTaskForm(type); + if (pluginForm) { + return pluginForm as FunctionComponent; + } + + // Core OSS task forms + switch (type) { + // System Tasks + case TaskType.EVENT: + return EventTaskForm; + case TaskType.HTTP: + return HTTPTaskForm as FunctionComponent; + case TaskType.HTTP_POLL: + return HTTPPollTaskForm; + case TaskType.JSON_JQ_TRANSFORM: + return JSONJQTransformForm; + case TaskType.INLINE: + return INLINETaskForm; + case TaskType.KAFKA_PUBLISH: + return KafkaTaskForm; + case TaskType.BUSINESS_RULE: + return BusinessRuleForm; + case TaskType.QUERY_PROCESSOR: + return QueryProcessorTaskForm; + case TaskType.GET_SIGNED_JWT: + return GetSignedJwtForm; + case TaskType.UPDATE_TASK: + return UpdateTaskForm; + case TaskType.MCP: + return MCPTaskForm; + + // Operators + case TaskType.DECISION: + case TaskType.SWITCH: + return SwitchOperatorForm; + case TaskType.DO_WHILE: + return DoWhileForm; + case TaskType.FORK_JOIN_DYNAMIC: + return DynamicForkOperatorForm; + case TaskType.DYNAMIC: + return DynamicOperatorForm; + case TaskType.TERMINATE: + return TerminateOperatorForm; + case TaskType.SET_VARIABLE: + return SetVariableOperatorForm; + case TaskType.SUB_WORKFLOW: + return SubWorkflowOperatorForm; + case TaskType.JOIN: + return JOINTaskForm; + case TaskType.WAIT: + return WaitTaskForm as FunctionComponent; + case TaskType.TERMINATE_WORKFLOW: + return TerminateWorkflowForm; + case TaskType.START_WORKFLOW: + return StartWorkflowTaskForm; + case TaskType.GET_WORKFLOW: + return GetWorkflowTaskForm; + case TaskType.YIELD: + return YieldTaskForm; + + // Alerting + case TaskType.OPS_GENIE: + return OpsGenieTaskForm; + + // Workers + case TaskType.SIMPLE: + return SimpleTaskForm; + case TaskType.JDBC: + return JDBCTaskForm; + + // Unknown task type - show generic JSON form + default: + return UnknownTaskForm; + } +}; + +const TaskForType: FunctionComponent = () => { + const { formTaskActor } = useContext(TaskFormContext); + const { workflowDefinitionActor } = useContext(WorkflowEditContext); + const [, send] = useActor(formTaskActor!); + const taskFormHeaderActor = useSelector( + formTaskActor!, + (state) => state.context.taskHeaderActor, + ); + + const taskType = useSelector( + formTaskActor!, + (state) => state.context.taskChanges.type, + ); + + const task = useSelector( + formTaskActor!, + (state) => state.context.taskChanges, + ); + + const collapseWorkflowList = useSelector( + workflowDefinitionActor!, + (state) => state.context.collapseWorkflowList, + ); + + const handleTaskChange = (taskChanges: Partial) => { + send({ type: FormMachineActionTypes.UPDATE_TASK, taskChanges }); + }; + const handleToggleExpand = (workflowName: string) => { + send({ + type: FormMachineActionTypes.UPDATE_COLLAPSE_WORKFLOW_LIST, + workflowName, + }); + handleTaskChange({}); + }; + + const TaskForTypeC = getTaskForm(taskType); + + return ( + { + handleTaskChange(task); + }} + onToggleExpand={(workflowName: string) => { + handleToggleExpand(workflowName); + }} + collapseWorkflowList={collapseWorkflowList} + taskFormHeaderActor={taskFormHeaderActor} + /> + ); +}; + +/** + * Core OSS task documentation URLs. + * Enterprise task doc URLs are registered via the plugin system. + */ +const coreTaskDocUrls: { [key: string]: string } = { + [TaskType.HTTP]: + "https://orkes.io/content/docs/reference-docs/system-tasks/http-task", + [TaskType.INLINE]: + "https://orkes.io/content/docs/reference-docs/system-tasks/inline-task", + [TaskType.JOIN]: "https://orkes.io/content/docs/reference-docs/join-task", + [TaskType.DO_WHILE]: + "https://orkes.io/content/docs/reference-docs/do-while-task", + [TaskType.FORK_JOIN]: + "https://orkes.io/content/docs/reference-docs/fork-task", + [TaskType.FORK_JOIN_DYNAMIC]: + "https://orkes.io/content/docs/reference-docs/dynamic-fork-task", + [TaskType.DYNAMIC]: + "https://orkes.io/content/docs/reference-docs/dynamic-task", + [TaskType.DECISION]: + "https://orkes.io/content/docs/reference-docs/decision-task", + [TaskType.KAFKA_PUBLISH]: + "https://orkes.io/content/docs/reference-docs/system-tasks/kafka-publish-task", + [TaskType.JSON_JQ_TRANSFORM]: + "https://orkes.io/content/docs/reference-docs/system-tasks/json-jq-transform-task", + [TaskType.SWITCH]: "https://orkes.io/content/docs/reference-docs/switch-task", + [TaskType.TERMINATE]: + "https://orkes.io/content/docs/reference-docs/terminate-task", + [TaskType.SET_VARIABLE]: + "https://orkes.io/content/docs/reference-docs/set-variable-task", + [TaskType.TERMINATE_WORKFLOW]: + "https://orkes.io/content/docs/reference-docs/system-tasks/terminate-workflow", + [TaskType.EVENT]: + "https://orkes.io/content/docs/reference-docs/system-tasks/event-task", + [TaskType.SUB_WORKFLOW]: + "https://orkes.io/content/docs/reference-docs/sub-workflow-task", + [TaskType.WAIT]: "https://orkes.io/content/docs/reference-docs/wait-task", + [TaskType.BUSINESS_RULE]: + "https://orkes.io/content/docs/reference-docs/system-tasks/business-rule", + [TaskType.START_WORKFLOW]: + "https://orkes.io/content/docs/reference-docs/start-workflow", + [TaskType.HTTP_POLL]: + "https://orkes.io/content/docs/reference-docs/system-tasks/http-poll-task", + [TaskType.SIMPLE]: "https://orkes.io/content/reference-docs/worker-task", + [TaskType.JDBC]: "https://orkes.io/content/reference-docs/system-tasks/jdbc", + [TaskType.QUERY_PROCESSOR]: + "https://orkes.io/content/reference-docs/system-tasks/query-processor", + [TaskType.OPS_GENIE]: + "https://orkes.io/content/reference-docs/system-tasks/opsgenie", + [TaskType.UPDATE_TASK]: + "https://orkes.io/content/reference-docs/system-tasks/update-task", + [TaskType.GET_WORKFLOW]: + "https://orkes.io/content/reference-docs/operators/get-workflow", + [TaskType.GET_SIGNED_JWT]: + "https://orkes.io/content/reference-docs/system-tasks/get-signed-jwt", + [TaskType.YIELD]: "https://orkes.io/content/reference-docs/operators/yield", +}; + +/** + * Get the documentation URL for a task type. + * First checks plugin-registered URLs, then falls back to core URLs. + */ +const getTaskDocUrl = (taskType: string): string | undefined => { + // First check plugin registry for enterprise doc URLs + const pluginUrl = pluginRegistry.getTaskDocUrl(taskType); + if (pluginUrl) { + return pluginUrl; + } + // Fall back to core URLs + return coreTaskDocUrls[taskType]; +}; + +const TaskFormContent: FunctionComponent = () => { + const { formTaskActor } = useContext(TaskFormContext); + const { isTrialExpired } = useAuth(); + const { workflowDefinitionActor } = useContext(WorkflowEditContext); + const panelRef = useRef(null); + const [panelWidth, setPanelWidth] = useState(0); + + const taskType = useSelector( + formTaskActor!, + (state) => state.context.originalTask.type, + ); + const isUnknownTaskType = !Object.values(TaskType).includes(taskType); + + const taskFormHeaderActor = useSelector( + formTaskActor!, + (state) => state.context.taskHeaderActor, + ); + + const task = useSelector( + formTaskActor!, + (state) => state.context.taskChanges, + ); + + const onChangeRequest = (value: any) => + formTaskActor!.send({ + type: FormMachineActionTypes.UPDATE_TASK, + taskChanges: updateField("inputParameters", value, task), + }); + + const taskDescription = + taskType && taskDescriptions[taskType as FormTaskType]; + + const tasksList = useSelector( + workflowDefinitionActor!, + (state) => state.context.workflowChanges?.tasks ?? [], + ); + + const truncate = useCallback( + (input: string) => { + let resultText = input; + if (panelWidth < 653 && panelWidth > 500) { + resultText = input.length > 10 ? `${input.substring(0, 10)}...` : input; + } else if (panelWidth < 500) { + resultText = input.length > 5 ? `${input.substring(0, 5)}...` : input; + } + return resultText; + }, + [panelWidth], + ); + useEffect(() => { + if (panelRef.current) { + const observer = new ResizeObserver((entries) => { + for (const entry of entries) { + setPanelWidth(entry.contentRect.width); + } + }); + + observer.observe(panelRef.current); + + // Cleanup function + return () => { + observer.disconnect(); + }; + } + }, []); + const handleTaskFieldUpdate = (fieldName: string) => (value: any) => { + formTaskActor!.send({ + type: FormMachineActionTypes.UPDATE_TASK, + taskChanges: { ...task, [fieldName]: value?.[fieldName] }, + }); + }; + + return ( + + + + + + {taskType} + + {taskType === TaskType.DECISION && ( + + + Deprecated + + )} + + SETTINGS + + + + {taskDescription ? ( + + +
    + {truncate(taskType)} Docs +
    + + } + /> + ) : ( + + +
    + {taskType} Docs +
    + + )} + {taskType !== TaskType.JOIN && ( + + + + )} +
    +
    + + + + + + + {isUnknownTaskType && ( + + + + + )} +
    + {ENABLE_TASK_STATS && } + {/**/} +
    +
    + ); +}; + +export default TaskFormContent; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskStats/CountBar.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskStats/CountBar.tsx new file mode 100644 index 0000000000..6aa465f838 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskStats/CountBar.tsx @@ -0,0 +1,50 @@ +import { FunctionComponent } from "react"; +import { Grid, Paper } from "@mui/material"; +import { State, ActorRef } from "xstate"; +import { useSelector } from "@xstate/react"; +import { TaskStatsMachineContext, TaskStatsEvents } from "./state"; + +interface CountBarProps { + taskStatsActor: ActorRef; +} + +export const CountBar: FunctionComponent = ({ + taskStatsActor, +}) => { + const [completed, failed, scheduled] = useSelector( + taskStatsActor, + (state: State) => [ + state.context.completedAmount, + state.context.failedAmount, + state.context.scheduledAmount, + ], + ); + return ( + + + + Current + + + {scheduled} +
    Scheduled
    +
    + + {completed} +
    Completed
    +
    + + {failed} +
    Failed
    +
    +
    +
    + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskStats/RangeButtons.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskStats/RangeButtons.tsx new file mode 100644 index 0000000000..11c974dc29 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskStats/RangeButtons.tsx @@ -0,0 +1,43 @@ +import { FunctionComponent } from "react"; +import Button from "components/MuiButton"; +import Stack from "@mui/material/Stack"; + +export interface RangeButtonsProps { + onChangeRange: (from: number) => void; + selected: number; +} + +const ONE_DAY = 24; +const THREE_DAYS = 3 * ONE_DAY; +const SEVEN_DAYS = 7 * ONE_DAY; + +export const RangeButtons: FunctionComponent = ({ + onChangeRange, + selected, +}) => { + return ( + + + + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskStats/TaskRateChart.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskStats/TaskRateChart.tsx new file mode 100644 index 0000000000..f2eb381c3d --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskStats/TaskRateChart.tsx @@ -0,0 +1,57 @@ +import { FunctionComponent } from "react"; +import { + Bar, + BarChart, + CartesianGrid, + Label, + Tooltip, + XAxis, + YAxis, +} from "recharts"; +import { formatUnixTimeToTimeString } from "utils/date"; +import { PrometheusRateData } from "./state"; + +export interface TaskRateChartProps { + color: string; + data: PrometheusRateData; + label: string; +} + +const dataToTimeCount = (data: PrometheusRateData) => + data.map(([timeStamp, count]) => ({ + time: formatUnixTimeToTimeString(timeStamp), + count, + })); + +export const TaskRateChart: FunctionComponent = ({ + data, + color = "#f34608", + label, +}) => { + return ( + + + + + + + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskStats/TaskStats.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskStats/TaskStats.tsx new file mode 100644 index 0000000000..a58a19bf35 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskStats/TaskStats.tsx @@ -0,0 +1,123 @@ +import { Box, CircularProgress, Grid, Paper } from "@mui/material"; +import { useActor, useSelector } from "@xstate/react"; +import { useContext } from "react"; +import { State } from "xstate"; +import { TaskFormContext } from "../state"; +import { CountBar } from "./CountBar"; +import { RangeButtons } from "./RangeButtons"; +import { TaskStatsEventTypes, TaskStatsMachineContext } from "./state"; +import { TaskRateChart } from "./TaskRateChart"; + +export const TaskStats = () => { + const { formTaskActor } = useContext(TaskFormContext); + //@ts-ignore + const taskStatsActor = formTaskActor?.children.get("taskStatsMachine"); + + const [, send] = useActor(taskStatsActor); + + const completedPromethusData = useSelector( + taskStatsActor, + (state: State) => { + return state.context.completedRateSeries; + }, + ); + + const failedPromethusData = useSelector( + taskStatsActor, + (state: State) => { + return state.context.failedRateSeries; + }, + ); + + const startHoursBack = useSelector( + taskStatsActor, + (state: State) => { + return state.context.startHoursBack; + }, + ); + + const isIdle = useSelector( + taskStatsActor, + (state: State) => state.matches("idle"), + ); + + const noStatsAvailable = useSelector( + taskStatsActor, + (state: State) => + state.matches("noStatsAvailable"), + ); + + const changeRange = (newRange: number) => { + send({ + type: TaskStatsEventTypes.CHANGE_START_TIME, + value: newRange, + }); + }; + + if (noStatsAvailable) { + return ( + + + Sorry! No stats available + + + ); + } + + return ( + + + TASK RATES + +
    + +
    + {isIdle ? ( + + + + + + + + + + + + ) : ( + + + + )} +
    + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskStats/state/actions.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskStats/state/actions.ts new file mode 100644 index 0000000000..2bb12a298e --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskStats/state/actions.ts @@ -0,0 +1,54 @@ +import { assign, DoneInvokeEvent } from "xstate"; +import { + TaskStatsMachineContext, + TaskStatsResponse, + ChangeTaskStatStartTimeEvent, + UpdateTaskNameEvent, +} from "./types"; +import _first from "lodash/first"; +import _last from "lodash/last"; + +type CurrentRes = { [key in "completed" | "failed"]: number }; + +export const persistMetrics = assign< + TaskStatsMachineContext, + DoneInvokeEvent +>((__context, { data: { completed, failed, current } }) => { + const currentMetric = current.data.result.reduce( + (acc, c): CurrentRes => ({ + ...acc, + [c.metric.status.toLowerCase() as "completed" | "failed"]: parseInt( + _last(c.value) as string, + 10, + ), + }), + { + completed: 0, + failed: 0, + }, + ); + return { + completedRateSeries: _first(completed.data.result)?.values || [], + failedRateSeries: _first(failed.data.result)?.values || [], + completedAmount: currentMetric?.completed, + failedAmount: currentMetric?.failed, + scheduledAmount: Object.values(currentMetric).reduce( + (acc, c) => acc + c, + 0, + ), + }; +}); + +export const persistStartTimeStamp = assign< + TaskStatsMachineContext, + ChangeTaskStatStartTimeEvent +>({ + startHoursBack: (__context, { value }) => value, +}); + +export const persistTaskName = assign< + TaskStatsMachineContext, + UpdateTaskNameEvent +>({ + taskName: (__, { name }) => name, +}); diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskStats/state/guards.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskStats/state/guards.ts new file mode 100644 index 0000000000..8b01710cc6 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskStats/state/guards.ts @@ -0,0 +1,6 @@ +import { TaskStatsMachineContext, UpdateTaskNameEvent } from "./types"; + +export const nameChanged = ( + { taskName }: TaskStatsMachineContext, + { name }: UpdateTaskNameEvent, +) => taskName !== name; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskStats/state/index.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskStats/state/index.ts new file mode 100644 index 0000000000..79fd82215f --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskStats/state/index.ts @@ -0,0 +1,2 @@ +export * from "./machine"; +export * from "./types"; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskStats/state/machine.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskStats/state/machine.ts new file mode 100644 index 0000000000..9a6a54946c --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskStats/state/machine.ts @@ -0,0 +1,94 @@ +import { createMachine } from "xstate"; +import { + TaskStatsMachineContext, + TaskStatsEventTypes, + TaskStatsEvents, +} from "./types"; +import * as services from "./services"; +import * as actions from "./actions"; +import * as guards from "./guards"; + +import { FEATURES, featureFlags } from "utils/flags"; + +const taskStatsEnabled = featureFlags.isEnabled(FEATURES.DISABLE_TASK_STATS); + +export const taskStatsMachine = createMachine< + TaskStatsMachineContext, + TaskStatsEvents +>( + { + id: "taskStatsMachine", + predictableActionArguments: true, + initial: "init", + context: { + // Context will be initialized by parent machine + completedRateSeries: [], + failedRateSeries: [], + completedAmount: 0, + failedAmount: 0, + startHoursBack: 0, + scheduledAmount: 0, + authHeaders: undefined, + taskName: "", + }, + states: { + notEnabled: {}, + init: { + always: [ + { cond: () => taskStatsEnabled, target: "fetchForStats" }, + { target: "notEnabled" }, + ], + }, + fetchForStats: { + invoke: { + src: "fetchForTaskMetrics", + onDone: { + actions: "persistMetrics", + target: "idle", + }, + onError: { + target: "noStatsAvailable", + }, + }, + }, + noStatsAvailable: { + on: { + [TaskStatsEventTypes.UPDATE_TASK_NAME]: { + actions: ["persistTaskName"], + target: "waitForName", + cond: "nameChanged", + }, + }, + }, + idle: { + on: { + [TaskStatsEventTypes.CHANGE_START_TIME]: { + actions: ["persistStartTimeStamp"], + target: "fetchForStats", + }, + [TaskStatsEventTypes.UPDATE_TASK_NAME]: { + actions: ["persistTaskName"], + target: "waitForName", + cond: "nameChanged", + }, + }, + }, + waitForName: { + after: { + 800: "fetchForStats", + }, + on: { + [TaskStatsEventTypes.UPDATE_TASK_NAME]: { + actions: ["persistTaskName"], + target: "waitForName", + }, + }, + }, + }, + }, + { + services, + actions: actions as any, + guards: guards as any, + }, +); diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskStats/state/services.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskStats/state/services.ts new file mode 100644 index 0000000000..c2ab0271eb --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskStats/state/services.ts @@ -0,0 +1,40 @@ +import { fetchContextNonHook, fetchWithContext } from "plugins/fetch"; +import { getCurrentUnixTimestamp, getUnixTimestampHoursAgo } from "utils/date"; +import { logger } from "utils/logger"; +import { queryClient } from "../../../../../../queryClient"; +import { TaskStatsMachineContext } from "./types"; + +const fetchContext = fetchContextNonHook(); + +const TASK_METRICS_PATH = `/metrics/task`; + +const RESOLUTION_FOR_24 = 0.05; + +export const fetchForTaskMetrics = async ({ + authHeaders: headers, + startHoursBack, + taskName, +}: TaskStatsMachineContext) => { + const timestampNow = getCurrentUnixTimestamp(); + const endTimeStamp = getUnixTimestampHoursAgo(startHoursBack, timestampNow); + + const step = (startHoursBack * RESOLUTION_FOR_24) / 24; + /* logger.info( */ + /* "Hittin prometheus proxy using statTimeStamp as ", */ + /* startTimestamp, */ + /* timestampNow */ + /* ); */ + const url = `${TASK_METRICS_PATH}/${taskName}?start=${endTimeStamp}&end=${timestampNow}&step=${Math.floor( + step * 60 * 60, + )}`; + try { + const response = await queryClient.fetchQuery( + [fetchContext.stack, url], + () => fetchWithContext(url, fetchContext, { headers }), + ); + return response; + } catch (error) { + logger.error("Fetching task list page", error); + return Promise.reject({ message: "Error fetching task list page" }); + } +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskStats/state/types.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskStats/state/types.ts new file mode 100644 index 0000000000..2cdf5f8bf7 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/TaskStats/state/types.ts @@ -0,0 +1,61 @@ +import { AuthHeaders } from "types/common"; + +export type PrometheusRateData = Array<[number, string]>; + +export interface RateTimeCount { + time: string; + count: number; +} + +export interface TaskStatsMachineContext { + completedRateSeries: PrometheusRateData; + failedRateSeries: PrometheusRateData; + completedAmount: number; + failedAmount: number; + scheduledAmount: number; + startHoursBack: number; + authHeaders?: AuthHeaders; + taskName: string; +} + +type Metric = { + taskType: string; + status: "COMPLETED" | "FAILED"; +}; + +type PrometheusResponse = { + data: { + result: Array<{ metric: Metric; values: PrometheusRateData }>; + }; +}; + +type PrometheusCurrentResponse = { + data: { + result: Array<{ metric: Metric; value: [number, string] }>; + }; +}; + +export interface TaskStatsResponse { + completed: PrometheusResponse; + failed: PrometheusResponse; + current: PrometheusCurrentResponse; +} + +export enum TaskStatsEventTypes { + CHANGE_START_TIME = "CHANGE_START_TIME", + UPDATE_TASK_NAME = "UPDATE_TASK_NAME", +} + +export type ChangeTaskStatStartTimeEvent = { + type: TaskStatsEventTypes.CHANGE_START_TIME; + value: number; +}; + +export type UpdateTaskNameEvent = { + type: TaskStatsEventTypes.UPDATE_TASK_NAME; + name: string; +}; + +export type TaskStatsEvents = + | ChangeTaskStatStartTimeEvent + | UpdateTaskNameEvent; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/ArrayForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/ArrayForm.tsx new file mode 100644 index 0000000000..0a6083555d --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/ArrayForm.tsx @@ -0,0 +1,101 @@ +import { Grid, Box } from "@mui/material"; +import { useCallback, useState, useMemo } from "react"; +import { Button, Input } from "components"; + +const ArrayForm = ({ + title = "Array", + addItemLabel = "Add Item", + valueColumnLabel = "Value", + value = [], + onChange = (_newValues) => {}, +}: { + title: string; + addItemLabel: string; + valueColumnLabel: string; + value?: Array; + onChange?: (newValues: Array) => void; +}) => { + const [localValues, setLocalValues] = useState>( + useMemo(() => value, [value]), + ); + + const addEmptyItem = useCallback(() => { + // generate random string of six characters + const suffix = Math.random().toString(36).substring(2, 7); + const newValues = [...localValues, `Some-Value-${suffix}`]; + setLocalValues(newValues); + onChange(newValues); + }, [localValues, onChange]); + + const deleteItem = useCallback( + (index: number) => { + const newValues = [...localValues]; + newValues.splice(index, 1); + + setLocalValues(newValues); + onChange(newValues); + }, + [localValues, onChange], + ); + + const replaceItem = useCallback( + (index: number, value: string) => { + const newValues = [...localValues]; + newValues[index] = value; + + setLocalValues(newValues); + onChange(newValues); + }, + [localValues, onChange], + ); + + return ( + + + +

    {title}

    +
    +
    + + {valueColumnLabel} + + {localValues && + localValues.map((value, index) => ( + + + { + replaceItem(index, newValue); + }} + value={value} + placeholder="e.g.: max-age=..." + /> + + + + + + ))} + + + + + +
    + ); +}; + +export default ArrayForm; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/BusinessRuleForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/BusinessRuleForm.tsx new file mode 100644 index 0000000000..656e752858 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/BusinessRuleForm.tsx @@ -0,0 +1,154 @@ +import { Box, Grid } from "@mui/material"; +import _get from "lodash/get"; + +import { ConductorArrayField } from "components/v1/ConductorArrayField"; +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import { ConductorFlatMapForm } from "components/v1/FlatMapForm/ConductorFlatMapForm"; +import { TaskType } from "types"; +import { ConductorCacheOutput } from "./ConductorCacheOutputForm"; +import { updateField } from "utils/fieldHelpers"; +import { Optional } from "./OptionalFieldForm"; +import TaskFormSection from "./TaskFormSection"; +import { TaskFormProps } from "./types"; +import { useGetSetHandler } from "./useGetSetHandler"; +import ConductorInput from "components/v1/ConductorInput"; + +const EXECUTION_STRATEGY_OPTIONS = ["FIRE_FIRST", "FIRE_ALL"]; + +const EXECUTION_STRATEGY_PATH = "inputParameters.executionStrategy"; +const INPUT_COLUMNS_PATH = "inputParameters.inputColumns"; +const OUTPUT_COLUMNS_PATH = "inputParameters.outputColumns"; +const ruleFileLocationPath = "inputParameters.ruleFileLocation"; +const cacheTimeoutMinutesPath = "inputParameters.cacheTimeoutMinutes"; + +export const BusinessRuleForm = (props: TaskFormProps) => { + const { task, onChange } = props; + const [executionStrategy, handlerExecutionStrategy] = useGetSetHandler( + props, + EXECUTION_STRATEGY_PATH, + ); + const [inputColumns, handleInputColumns] = useGetSetHandler( + props, + INPUT_COLUMNS_PATH, + ); + const [outputColumns, handleOutputColumns] = useGetSetHandler( + props, + OUTPUT_COLUMNS_PATH, + ); + + return ( + + + + + + onChange(updateField(ruleFileLocationPath, changes, task)) + } + /> + + + + + + + + Note: When you update the rules file, there is a default + refresh interval of 60 mins. This can cause any updated rules to + reflect only after a delay of up to 60 minutes. Override the refresh + interval value below to adjust this delay to the required number of + minutes. + + + + + + + onChange( + updateField(cacheTimeoutMinutesPath, parseInt(changes), task), + ) + } + /> + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/ChunkTextTaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/ChunkTextTaskForm.tsx new file mode 100644 index 0000000000..8eefe03688 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/ChunkTextTaskForm.tsx @@ -0,0 +1,578 @@ +import { + Box, + FormControlLabel, + FormHelperText, + Grid, + IconButton, + MenuItem, + Select, + Slider, + Switch, + Tooltip, + Typography, +} from "@mui/material"; +import { assoc as _assoc, pipe as _pipe } from "lodash/fp"; +import { useCallback, useContext, useMemo, useState } from "react"; +import ClearIcon from "@mui/icons-material/Clear"; +import ContentPasteIcon from "@mui/icons-material/ContentPaste"; + +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import { ConductorCacheOutput } from "./ConductorCacheOutputForm"; +import { Optional } from "./OptionalFieldForm"; +import TaskFormSection from "./TaskFormSection"; +import { TaskFormProps } from "./types"; +import { updateField } from "utils/fieldHelpers"; +import ConductorInput from "components/v1/ConductorInput"; +import { ConductorCodeBlockInput } from "components/v1/ConductorCodeBlockInput"; +import { ColorModeContext } from "theme/material/ColorModeContext"; + +// Comprehensive media type options with auto-detect +const CHUNK_TEXT_MEDIA_TYPES = [ + { value: "auto", label: "Auto-detect (Recommended)", category: "Default" }, + // Code Languages - General Purpose + { value: ".java", label: "Java (.java)" }, + { value: ".js", label: "JavaScript (.js)" }, + { value: ".ts", label: "TypeScript (.ts)" }, + { value: ".py", label: "Python (.py)" }, + { value: ".go", label: "Go (.go)" }, + { value: ".cpp", label: "C++ (.cpp)" }, + { value: ".c", label: "C (.c)" }, + { value: ".cs", label: "C# (.cs)" }, + { value: ".php", label: "PHP (.php)" }, + { value: ".rb", label: "Ruby (.rb)" }, + { value: ".swift", label: "Swift (.swift)" }, + { value: ".kt", label: "Kotlin (.kt)" }, + // Code Languages - Web & Markup + { value: ".html", label: "HTML (.html)" }, + { value: ".css", label: "CSS (.css)" }, + { value: ".scss", label: "SCSS (.scss)" }, + { value: ".less", label: "LESS (.less)" }, + { value: ".xml", label: "XML (.xml)" }, + { value: ".yaml", label: "YAML (.yaml)" }, + { value: ".json", label: "JSON (.json)" }, + { value: ".sql", label: "SQL (.sql)" }, + // Text Formats + { value: "text/plain", label: "Plain Text" }, + { value: "text/markdown", label: "Markdown" }, + { value: "text/html", label: "HTML" }, + // Document Types + { + value: "application/pdf", + label: "PDF Document", + }, + { value: "text/rtf", label: "Rich Text Format" }, +]; + +const CODE_EXTENSIONS = [ + ".java", + ".js", + ".ts", + ".py", + ".go", + ".cpp", + ".c", + ".cs", + ".php", + ".rb", + ".swift", + ".kt", + ".html", + ".css", + ".scss", + ".less", + ".xml", + ".yaml", + ".json", + ".sql", +]; +const CODE_EXTENSIONS_FOR_CHUNK_SIZE = [ + ".java", + ".js", + ".ts", + ".py", + ".go", + ".cpp", + ".c", + ".cs", + ".php", + ".rb", + ".swift", + ".kt", +]; + +const getChunkingStrategyDescription = (mediaType: string) => { + if (!mediaType || mediaType === "auto") { + return "Text will be automatically analyzed to detect the best chunking strategy based on content structure."; + } + + if (CODE_EXTENSIONS.includes(mediaType)) { + return "Code will be chunked with language-specific semantics, preserving function boundaries, class definitions, and logical code blocks."; + } + + if ( + mediaType.startsWith("text/") || + mediaType === "application/pdf" || + mediaType === "text/rtf" + ) { + return "Text will be chunked based on natural language boundaries like paragraphs, sentences, and semantic breaks."; + } + + return "Content will be chunked using general text chunking strategies."; +}; + +const estimateChunkCount = (text: string, chunkSize: number): number => { + if (!text || !chunkSize || chunkSize <= 0) return 0; + const textLength = text.length; + return Math.ceil(textLength / chunkSize); +}; + +const getChunkSizeRecommendation = (mediaType: string): string => { + if (CODE_EXTENSIONS_FOR_CHUNK_SIZE.includes(mediaType)) { + return "Recommended: 1500-2000 characters for code to preserve function/class boundaries"; + } + + if ( + mediaType.startsWith("text/") || + mediaType === "text/plain" || + mediaType === "text/markdown" + ) { + return "Recommended: 800-1200 characters for natural language text"; + } + + if (mediaType === "application/pdf" || mediaType === "text/rtf") { + return "Recommended: 1000-1500 characters for document content"; + } + + return "Recommended: 1024 characters (default)"; +}; + +// Language options for syntax highlighting +const SYNTAX_LANGUAGES = [ + { value: "plaintext", label: "Plain Text" }, + { value: "javascript", label: "JavaScript" }, + { value: "typescript", label: "TypeScript" }, + { value: "python", label: "Python" }, + { value: "java", label: "Java" }, + { value: "go", label: "Go" }, + { value: "cpp", label: "C++" }, + { value: "c", label: "C" }, + { value: "csharp", label: "C#" }, + { value: "php", label: "PHP" }, + { value: "ruby", label: "Ruby" }, + { value: "swift", label: "Swift" }, + { value: "kotlin", label: "Kotlin" }, + { value: "rust", label: "Rust" }, + { value: "html", label: "HTML" }, + { value: "css", label: "CSS" }, + { value: "scss", label: "SCSS" }, + { value: "xml", label: "XML" }, + { value: "yaml", label: "YAML" }, + { value: "json", label: "JSON" }, + { value: "sql", label: "SQL" }, + { value: "markdown", label: "Markdown" }, +]; + +export const ChunkTextTaskForm = ({ task, onChange }: TaskFormProps) => { + const [textCharCount, setTextCharCount] = useState(0); + const [syntaxHighlighting, setSyntaxHighlighting] = useState(false); + const [selectedLanguage, setSelectedLanguage] = useState("plaintext"); + const { mode } = useContext(ColorModeContext); + + // Get current values from task + const text = task.inputParameters?.text || ""; + const chunkSize = task.inputParameters?.chunkSize || 1024; + const mediaType = task.inputParameters?.mediaType || "auto"; + + // Update character count whenever text changes + useMemo(() => { + setTextCharCount(text.length); + }, [text]); + + // Calculate estimated chunks + const estimatedChunks = useMemo( + () => estimateChunkCount(text, chunkSize), + [text, chunkSize], + ); + + const handleTextChange = useCallback( + (value: string) => { + onChange(updateField("inputParameters.text", value, task)); + }, + [onChange, task], + ); + + const handleChunkSizeChange = useCallback( + (value: number) => { + onChange(updateField("inputParameters.chunkSize", value, task)); + }, + [onChange, task], + ); + + const handleMediaTypeChange = useCallback( + (value: string) => { + onChange(updateField("inputParameters.mediaType", value, task)); + }, + [onChange, task], + ); + + const handleClearText = useCallback(() => { + handleTextChange(""); + }, [handleTextChange]); + + const handlePasteText = useCallback(async () => { + try { + const clipboardText = await navigator.clipboard.readText(); + handleTextChange(clipboardText); + } catch (err) { + console.error("Failed to read clipboard:", err); + } + }, [handleTextChange]); + + // Slider marks for chunk size + const sliderMarks = [ + { value: 100, label: "100" }, + { value: 2500, label: "2.5K" }, + { value: 5000, label: "5K" }, + { value: 7500, label: "7.5K" }, + { value: 10000, label: "10K" }, + ]; + + return ( + + {/* Text Input Section */} + + + + + setSyntaxHighlighting(e.target.checked)} + /> + } + label="Enable syntax highlighting" + /> + + + + + {syntaxHighlighting ? ( + + + + {/* Language selector positioned on top border */} + + + + + + + + + + + + + + + + + + + + ) : ( + + + + + + + + + + + + + + + + + + + + )} + + + + + {/* Chunk Size Section */} + + + + + + { + const numValue = parseInt(value, 10); + if (!isNaN(numValue) && numValue > 0 && numValue <= 10000) { + handleChunkSizeChange(numValue); + } + }} + fullWidth + error={chunkSize < 100 || chunkSize > 10000} + helperText="Enter a value between 100 and 10000" + inputProps={{ min: 100, max: 10000 }} + /> + + + + + + Estimated chunks: {estimatedChunks} + + + Characters per chunk: {chunkSize} + + + + + + + + + handleChunkSizeChange(value as number)} + min={100} + max={10000} + step={100} + marks={sliderMarks} + valueLabelDisplay="auto" + sx={{ mt: 2 }} + /> + + + + + + {getChunkSizeRecommendation(mediaType)} + + + + + + {/* Media Type Section */} + + + + handleMediaTypeChange(value || "auto")} + value={mediaType} + otherOptions={CHUNK_TEXT_MEDIA_TYPES.map((opt) => opt.value)} + label="Media Type" + helperText="Select a media type or use auto-detect to automatically determine the best chunking strategy" + getOptionLabel={(option) => + CHUNK_TEXT_MEDIA_TYPES.find((opt) => opt.value === option) + ?.label ?? option.toString() + } + renderOption={(props, option) => ( + + + {CHUNK_TEXT_MEDIA_TYPES.find((opt) => opt.value === option) + ?.label ?? option} + + + )} + /> + + + + + + Chunking Strategy:{" "} + {getChunkingStrategyDescription(mediaType)} + + + + + + + + + + + + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/ConductorCacheOutputForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/ConductorCacheOutputForm.tsx new file mode 100644 index 0000000000..002bb76a57 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/ConductorCacheOutputForm.tsx @@ -0,0 +1,107 @@ +import { Box, FormControlLabel, Grid, Link, Switch } from "@mui/material"; +import ConductorInput from "components/v1/ConductorInput"; +import { path as _path } from "lodash/fp"; +import { FunctionComponent, useState } from "react"; +import { updateField } from "utils/fieldHelpers"; +import ConductorFlexibleAutoCompleteVariables from "./ConductorFlexibleAutoCompleteVariables"; + +interface ConductorCacheOutputProps { + onChange: (value: any) => void; + taskJson: any; +} + +const ttlPath = "cacheConfig.ttlInSecond"; +const cacheKeyPath = "cacheConfig.key"; + +export const ConductorCacheOutput: FunctionComponent< + ConductorCacheOutputProps +> = ({ onChange, taskJson }) => { + const cacheKeyOptions = taskJson?.inputParameters + ? Object.keys(taskJson?.inputParameters).map((item) => `\${${item}}`) + : []; + const ttl = _path(ttlPath, taskJson); + const cacheKey = _path(cacheKeyPath, taskJson); + const changeTtl = (value: any) => { + onChange(updateField(ttlPath, value, taskJson)); + }; + const changeCacheKey = (value: any) => { + onChange(updateField(cacheKeyPath, value, taskJson)); + }; + const [show, setShow] = useState(ttl ? true : false); + const handleSetShow = () => { + if (show) { + onChange({ ...taskJson, cacheConfig: undefined }); + setShow(false); + } else { + setShow(true); + } + }; + return ( + + + + } + label={ + + + Cache Output + + + Learn more + + + } + /> + + + + When turned on, cache outputs can be saved for reuse in subsequent task + executions. + + {show && ( + + + + + + + + + )} + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/ConductorFlexibleAutoCompleteVariables.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/ConductorFlexibleAutoCompleteVariables.tsx new file mode 100644 index 0000000000..7d02dfc6d0 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/ConductorFlexibleAutoCompleteVariables.tsx @@ -0,0 +1,72 @@ +import { Box, Grid } from "@mui/material"; +import match from "autosuggest-highlight/match"; +import parse from "autosuggest-highlight/parse"; +import { SyntheticEvent } from "react"; + +import { ConductorAutoComplete } from "components/v1"; + +const ConductorFlexibleAutoCompleteVariables = ({ + options = [], + value = "", + onChange = (_newValues) => {}, + label = "", +}: { + options: Array; + value?: string; + onChange?: (newValues: string) => void; + label?: string; +}) => { + const handleChange = (e: any, data: string) => { + onChange(data); + }; + + const handleSelect = (__: SyntheticEvent, data: any) => { + let result = ""; + if (data) { + result = value + data.toString(); + } + onChange(result); + }; + + return ( + + + + option} + renderOption={(props, option, { inputValue }) => { + const matches = match(option as string, inputValue); + const parts = parse(option as string, matches); + return ( +
  • + {parts.map((part, index) => ( + + {part.text} + + ))} +
  • + ); + }} + // renderInput={(params) => } + value={value} + onChange={handleSelect} + onInputChange={handleChange} + /> +
    +
    +
    + ); +}; + +export default ConductorFlexibleAutoCompleteVariables; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/ConductorObjectOrStringInput.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/ConductorObjectOrStringInput.tsx new file mode 100644 index 0000000000..37164eda2c --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/ConductorObjectOrStringInput.tsx @@ -0,0 +1,104 @@ +import { Grid } from "@mui/material"; +import { ConductorCodeBlockInput } from "components/v1/ConductorCodeBlockInput"; +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import { FunctionComponent } from "react"; +import { FIELD_TYPE_OBJECT, FIELD_TYPE_STRING, FieldType } from "types/common"; +import { + DEFAULT_FIELD_VALUES_CONF, + ValueInputDefaultValues, + castToType, + inferType, + useCoerceToObject, +} from "utils"; +import FieldTypeDropdown from "./FieldTypeDropdown"; + +interface DynamicInputProps { + isContainsError: boolean; + onChangeValue: (value: any) => void; + type: FieldType; + value: any; + valueColumnLabel?: string; + dropDownOptions?: string[]; +} + +const DynamicInput = ({ + isContainsError, + onChangeValue, + type, + value, + valueColumnLabel = "", +}: DynamicInputProps) => { + const [onObjChange, objValue, cantCoerce] = useCoerceToObject( + onChangeValue, + value, + ); + switch (type) { + case FIELD_TYPE_OBJECT: + return ( + + ); + + default: + return ( + onChangeValue(castToType(val, inferType(value)))} + helperText={isContainsError ? " " : ""} + /> + ); + } +}; + +interface ValueInputProps { + onChangeValue: (a: string) => void; + value: string | object; + valueLabel?: string; + defaultObjectValue?: ValueInputDefaultValues; + dropDownOptions?: string[]; +} + +export const ConductorObjectOrStringInput: FunctionComponent< + ValueInputProps +> = ({ + onChangeValue, + value, + valueLabel = "Value", + defaultObjectValue = DEFAULT_FIELD_VALUES_CONF, + dropDownOptions = [], +}) => { + const currentType = inferType(value); + const typeLabel = valueLabel ? `${valueLabel} type` : "Type"; + + return ( + + + + + + + { + onChangeValue(castToType(value, type, defaultObjectValue)); + }} + allowedTypes={[FIELD_TYPE_STRING, FIELD_TYPE_OBJECT]} + /> + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/ConductorValueInput.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/ConductorValueInput.tsx new file mode 100644 index 0000000000..be8f22dc2d --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/ConductorValueInput.tsx @@ -0,0 +1,107 @@ +import { Grid, SxProps } from "@mui/material"; +import _isEmpty from "lodash/isEmpty"; +import { FunctionComponent } from "react"; + +import { ConductorAutoComplete } from "components/v1"; +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import { FIELD_TYPE_OBJECT, FIELD_TYPE_STRING, FieldType } from "types/common"; +import { + DEFAULT_FIELD_VALUES_CONF, + ValueInputDefaultValues, + castToType, + inferType, +} from "utils"; +import FieldTypeDropdown from "./FieldTypeDropdown"; + +interface DynamicInputProps { + isContainsError: boolean; + onChangeValue: (value: any) => void; + type: FieldType; + value: any; + valueColumnLabel?: string; + dropDownOptions?: string[]; +} + +const DynamicInput = ({ + isContainsError, + onChangeValue, + type, + value, + valueColumnLabel = "", + dropDownOptions = [], +}: DynamicInputProps) => { + switch (type) { + case FIELD_TYPE_OBJECT: + return ( + { + onChangeValue(val); + }} + value={value} + /> + ); + + default: + return ( + onChangeValue(castToType(val, inferType(value)))} + helperText={isContainsError ? " " : ""} + /> + ); + } +}; + +interface ValueInputProps { + onChangeValue: (a: string) => void; + value: string | boolean; + valueLabel?: string; + defaultObjectValue?: ValueInputDefaultValues; + dropDownOptions?: string[]; + keyStyle?: SxProps; + valueStyle?: SxProps; +} + +export const ConductorValueInput: FunctionComponent = ({ + onChangeValue, + value, + valueLabel = "Value", + defaultObjectValue = DEFAULT_FIELD_VALUES_CONF, + dropDownOptions = [], + keyStyle = {}, + valueStyle = {}, +}) => { + const currentType = inferType(value); + const typeLabel = valueLabel ? `${valueLabel} type` : "Type"; + + return ( + + + + + + { + onChangeValue(castToType(value, type, defaultObjectValue)); + }} + allowedTypes={[FIELD_TYPE_STRING, FIELD_TYPE_OBJECT]} + /> + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/DoWhileTaskForm/DoWhileCodeBlock.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/DoWhileTaskForm/DoWhileCodeBlock.tsx new file mode 100644 index 0000000000..d907107eae --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/DoWhileTaskForm/DoWhileCodeBlock.tsx @@ -0,0 +1,225 @@ +import { EditorProps, Monaco } from "@monaco-editor/react"; +import { BoxProps } from "@mui/material"; +import { Theme } from "@mui/material/styles"; +import { SxProps } from "@mui/system"; +import _keys from "lodash/keys"; +import { + CSSProperties, + FunctionComponent, + MutableRefObject, + ReactNode, + useCallback, + useContext, + useEffect, + useRef, +} from "react"; + +import { ConductorCodeBlockInput } from "components/v1/ConductorCodeBlockInput"; +import { + invalidDollarVariables, + undeclaredInputParameters, +} from "pages/definition/helpers"; +import { ColorModeContext } from "theme/material/ColorModeContext"; +import { DoWhileTaskDef } from "types"; +import { + OnlyTheWordInfoProp, + editorAddCommandAltEnter, + editorDecorations, +} from "../../helpers"; +import { smallEditorDefaultOptions } from "../editorConfig"; + +type DoWhileCodeBlockProps = { + label?: ReactNode; + language?: string; + onChange?: (taskChanges: Partial) => void; + containerProps?: BoxProps; + error?: boolean; + height?: number | "auto"; + minHeight?: number; + autoformat?: boolean; + labelStyle?: SxProps; + languageLabel?: string; + containerStyles?: CSSProperties; + autoSizeBox?: boolean; + task: Partial; +} & Partial>; + +const additionalEditorOptions = { + lineNumbers: "on" as const, + lineDecorationsWidth: 10, +}; + +const warnUndeclaredVariables = ( + editor: Monaco, + monaco: any, + task: Partial, + currentDecorations: MutableRefObject, +) => { + const model = editor.getModel(); + const taskExpression = task?.loopCondition; + const taskReferenceName = task?.taskReferenceName ?? ""; + const loopOverTasks = + task?.loopOver?.map((item) => item.taskReferenceName) ?? []; + if (model && taskExpression && editor) { + const addedInputParameters = undeclaredInputParameters( + model.getValue(), + task?.inputParameters, + ); + if (addedInputParameters.includes(taskReferenceName)) { + addedInputParameters.splice( + addedInputParameters.indexOf(taskReferenceName), + 1, + ); + } + const filteredAddedInputParameters = addedInputParameters.filter( + (element) => !loopOverTasks.includes(element), + ); + + const invalidDollarVars = invalidDollarVariables(model.getValue()); + + const decorations = editorDecorations( + model, + [...filteredAddedInputParameters, ...invalidDollarVars], + monaco, + ); + + return editor.deltaDecorations( + currentDecorations.current ? currentDecorations.current : [], + decorations.flat(), + ); + } +}; + +export const DoWhileCodeBlock: FunctionComponent = ({ + language = "json", + onChange = () => null, + autoSizeBox = false, + task, + ...restOfProps +}) => { + const taskRef = useRef | null>(null); + taskRef.current = task; + const { mode } = useContext(ColorModeContext); + const disposeRef = useRef void)>(null); + const currentDecorations = useRef([]) as any; + + useEffect(() => { + return () => { + if (disposeRef.current) { + disposeRef.current(); + } + }; + }, []); + + const handleEditorDidMount = useCallback( + (editor: Monaco, monaco: any) => { + const callBackFunction = (onlyTheWordInfo: OnlyTheWordInfoProp) => { + onChange({ + ...taskRef.current, + inputParameters: { + ...taskRef.current!.inputParameters, + [onlyTheWordInfo.word]: "", // Add the original word + }, + } as Partial); + // cleanup + currentDecorations.current = warnUndeclaredVariables( + editor, + monaco, + taskRef.current!, + currentDecorations, + ); + }; + // editor.AddCommand function + editorAddCommandAltEnter(editor, monaco, taskRef, callBackFunction); + + editor.onDidChangeModelContent((_event: any) => { + // Warn on change + currentDecorations.current = warnUndeclaredVariables( + editor, + monaco, + taskRef.current!, + currentDecorations, + ); + }); + + currentDecorations.current = warnUndeclaredVariables( + editor, + monaco, + taskRef.current!, + currentDecorations, + ); + }, + [onChange], + ); + + const onEditorChange = useCallback( + (editorValue: string) => { + onChange({ + ...taskRef.current, + loopCondition: editorValue, + } as Partial); + }, + [onChange], + ); + + return ( + { + handleEditorDidMount(editor, monaco); + }} + beforeMount={(monaco: Monaco) => { + if (disposeRef.current) { + disposeRef.current(); + disposeRef.current = null; + } + const disposable = monaco.languages.registerCompletionItemProvider( + "javascript", + { + provideCompletionItems: () => { + const inputVariables = _keys(taskRef?.current?.inputParameters); + const loopOverTasks = + taskRef?.current?.loopOver?.map( + (item) => `$.${item.taskReferenceName}`, + ) ?? []; + + let variableSuggestions: string[] = []; + if (inputVariables) { + variableSuggestions = inputVariables + .filter( + (item) => item !== "expression" && item !== "evaluatorType", + ) + .map((item) => `$.${item}`); + } + // Provide suggestions for JSON properties that start with the current text + const propertySuggestions = [ + ...variableSuggestions, + ...loopOverTasks, + ].map((property) => ({ + label: property, + kind: monaco.languages.CompletionItemKind.Value, + insertText: `${property}`, + })); + + // Merge custom suggestions with JSON property suggestions + const suggestions = [...propertySuggestions]; + return { suggestions }; + }, + }, + ); + // IMPORTANT: keep `dispose()` bound to its disposable context. + // Destructuring `dispose` can lose `this` and throw "Unbound disposable context". + disposeRef.current = () => disposable.dispose(); + }} + defaultLanguage={language} + options={{ + ...smallEditorDefaultOptions, + ...(autoSizeBox && { scrollBeyondLastLine: false }), + ...additionalEditorOptions, + }} + value={taskRef?.current?.loopCondition || ""} + {...restOfProps} + /> + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/DoWhileTaskForm/DoWhileForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/DoWhileTaskForm/DoWhileForm.tsx new file mode 100644 index 0000000000..b893f5ef10 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/DoWhileTaskForm/DoWhileForm.tsx @@ -0,0 +1,289 @@ +import { Box, FormControlLabel, Grid } from "@mui/material"; +import { useSelector } from "@xstate/react"; +import MuiCheckbox from "components/MuiCheckbox"; +import RadioButtonGroup from "components/RadioButtonGroup"; +import ConfirmChoiceDialog from "components/ConfirmChoiceDialog"; +import { ConductorAutoComplete } from "components/v1"; +import ConductorInput from "components/v1/ConductorInput"; +import ConductorInputNumber from "components/v1/ConductorInputNumber"; +import { ConductorFlatMapFormBase } from "components/v1/FlatMapForm/ConductorFlatMapForm"; +import _capitalize from "lodash/capitalize"; +import _first from "lodash/first"; +import _isNull from "lodash/isNull"; +import _omit from "lodash/omit"; +import { WorkflowEditContext } from "pages/definition/state"; +import { useCallback, useContext, useState } from "react"; +import { colors } from "theme/tokens/variables"; +import { CommonTaskDef, DoWhileTaskDef } from "types/TaskType"; +import { TaskDef, TaskType } from "types/common"; +import { getSequentiallySuffix } from "utils"; +import { filterOptionByEvaluatorType } from "utils/deprecatedRadioFilter"; +import { Optional } from "../OptionalFieldForm"; +import TaskFormSection from "../TaskFormSection"; +import { TaskFormProps } from "../types"; +import { DoWhileCodeBlock } from "./DoWhileCodeBlock"; +import { useDoWhileHandler } from "./common"; +import { genSampleScripts } from "./sampleScripts"; + +export const DoWhileForm = ({ task, onChange }: TaskFormProps) => { + const { workflowDefinitionActor } = useContext(WorkflowEditContext); + + const editorTasks: TaskDef[] = useSelector( + workflowDefinitionActor!, + (state) => state.context?.workflowChanges?.tasks, + ); + + const loopOverTasks = useCallback(() => { + const result: CommonTaskDef[] = []; + + if (editorTasks) { + editorTasks.forEach((editorTask) => { + if ( + editorTask.type === TaskType.DO_WHILE && + editorTask?.loopOver?.length + ) { + result.push(...editorTask.loopOver); + } + }); + } + + return result; + }, [editorTasks]); + + const { + handleNoLimitChange, + handleKeepLastNChange, + handleRadioButtonChange, + onInputParameterChange, + onLoopConditionChange, + } = useDoWhileHandler({ + task, + onChange, + }); + + const radioOptions = filterOptionByEvaluatorType(task?.evaluatorType); + + const keepLastN = task.inputParameters?.keepLastN; + + const sampleScripts = genSampleScripts(task as DoWhileTaskDef); + + const [selectedScriptOption, setSelectedScriptOption] = useState< + string | null + >(null); + + const handleChangeSampleScripts = () => { + const sampleScriptsValues = Object.values(sampleScripts); + const selectedValue = sampleScriptsValues.find( + (value) => value.loopCondition.trim() === selectedScriptOption?.trim(), + ); + const nonSelectedValue = sampleScriptsValues.find( + (value) => value.loopCondition.trim() !== selectedScriptOption?.trim(), + ); + + if (selectedValue) { + // Change loop over task name & task ref name + const updatedLoopOver = + selectedValue.loopOver?.map((item) => { + const { name, taskReferenceName } = getSequentiallySuffix({ + name: item.taskReferenceName, + refNames: loopOverTasks().map((item) => item.taskReferenceName), + }); + + return { + ...item, + name, + taskReferenceName, + }; + }) || []; + + const fistLoopOverTask = _first(task?.loopOver); + const isExampleTaskExisted = + fistLoopOverTask?.taskReferenceName?.startsWith( + selectedValue.loopOver?.[0]?.taskReferenceName, + ); + + let updatedLoopOverTasks = [...(task?.loopOver || [])]; + let updatedInputParameters = task?.inputParameters + ? { ...task?.inputParameters } + : {}; + + const inputKeys = nonSelectedValue + ? Object.keys(nonSelectedValue?.inputParameters) + : []; + + // Iterate over array + // Don't need to add more example task + if (!isExampleTaskExisted) { + updatedLoopOverTasks = [...updatedLoopOver, ...updatedLoopOverTasks]; + } + + if (inputKeys) { + updatedInputParameters = _omit(updatedInputParameters, inputKeys); + } + + onChange({ + ...task, + loopCondition: selectedValue?.loopCondition, + inputParameters: { + ...selectedValue?.inputParameters, + ...updatedInputParameters, + }, + loopOver: updatedLoopOverTasks, + }); + } + setSelectedScriptOption(null); + }; + + const handleShowConfirmOverrideDialog = (option: string) => { + if (option) { + const maybeSelectedScript = option + .toLowerCase() + .replaceAll(" ", "_") as "fixed_number"; + if (maybeSelectedScript != null) { + const selectedScript = sampleScripts[maybeSelectedScript].loopCondition; + const isSameScript = + selectedScript.replaceAll(" ", "") === + task?.loopCondition?.replaceAll(" ", ""); + setSelectedScriptOption(isSameScript ? null : selectedScript); + } + } + }; + + return ( + + + + + + + + + + + + + } + label="Loop condition:" + sx={{ + marginLeft: 0, + "& .MuiFormControlLabel-label": { + fontWeight: 600, + color: colors.gray07, + }, + }} + /> + + + {["javascript", "graaljs"].includes( + task.evaluatorType as string, + ) && ( + _capitalize(`${key.replaceAll("_", " ")}`) as any, + )} + onChange={(__, value: any) => + handleShowConfirmOverrideDialog(value) + } + data-testid="do-while-sample-scripts-dropdown" + /> + )} + + + {["javascript", "graaljs"].includes( + task.evaluatorType as string, + ) ? ( + } + onChange={onChange} + /> + ) : ( + + )} + + + + + + + } + label="No Limits" + sx={{ + marginLeft: 6, + alignSelf: "center", + "& .MuiFormControlLabel-label": { + fontWeight: 600, + color: "#767676", + }, + }} + /> + + + + + + + + + + {selectedScriptOption != null && ( + { + if (confirmed) { + handleChangeSampleScripts(); + } else { + setSelectedScriptOption(null); + } + }} + message={ + "Applying the sample script will overwrite any existing script. Are you sure you want to proceed?" + } + /> + )} + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/DoWhileTaskForm/common.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/DoWhileTaskForm/common.ts new file mode 100644 index 0000000000..270867e4be --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/DoWhileTaskForm/common.ts @@ -0,0 +1,53 @@ +import { ChangeEvent } from "react"; +import { updateField } from "utils/fieldHelpers"; +import { TaskFormProps } from "../types"; + +export const useDoWhileHandler = ({ task, onChange }: TaskFormProps) => { + const handleNoLimitChange = (event: ChangeEvent) => { + const { checked } = event.target; + onChange( + checked + ? { + ...task, + inputParameters: { + ...task.inputParameters, + keepLastN: undefined, + }, + } + : { + ...task, + inputParameters: { + ...task.inputParameters, + keepLastN: 2, + }, + }, //the form machine will remove the attribute when set to undefined + ); + }; + + const handleRadioButtonChange = ( + _evt: ChangeEvent, + val: string, + ) => onChange(updateField("evaluatorType", val, task)); + + const handleKeepLastNChange = (val: any) => { + onChange(updateField("inputParameters.keepLastN", val, task)); + }; + + const onInputParameterChange = (newValue: Record) => + onChange(updateField("inputParameters", newValue, task)); + + const onLoopConditionChange = (val: string) => + onChange(updateField("loopCondition", val, task)); + + const onChangeOptional = (event: ChangeEvent) => + onChange(updateField("optional", event.target.checked, task)); + + return { + handleNoLimitChange, + handleKeepLastNChange, + handleRadioButtonChange, + onInputParameterChange, + onLoopConditionChange, + onChangeOptional, + }; +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/DoWhileTaskForm/index.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/DoWhileTaskForm/index.ts new file mode 100644 index 0000000000..e7ed32266b --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/DoWhileTaskForm/index.ts @@ -0,0 +1 @@ +export * from "./DoWhileForm"; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/DoWhileTaskForm/sampleScripts.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/DoWhileTaskForm/sampleScripts.ts new file mode 100644 index 0000000000..840153528d --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/DoWhileTaskForm/sampleScripts.ts @@ -0,0 +1,40 @@ +import { DoWhileTaskDef } from "types/TaskType"; +import { TaskDef } from "types/common"; + +export const genSampleScripts = (task: DoWhileTaskDef | undefined) => ({ + fixed_number: { + loopCondition: `(function () {\n if (${ + task?.taskReferenceName + ? `$.${task?.taskReferenceName}` + : `$.do_while_ref` + }['iteration'] < $.number) {\n return true;\n }\n return false;\n})();`, + inputParameters: { number: 5 }, + loopOver: [], + }, + iterate_over_array: { + loopCondition: `(function () {\n if (${ + task?.taskReferenceName + ? `$.${task?.taskReferenceName}` + : `$.do_while_ref` + }['iteration'] < $.myArray.length) {\n return true;\n }\n return false;\n})();`, + inputParameters: { myArray: [{ name: "Orkes" }, { year: 2024 }] }, + loopOver: [ + { + name: "inline_sample", + taskReferenceName: "inline_sample_ref", + type: "INLINE", + inputParameters: { + expression: + "(function () { \n const current = $.iteration;\n return current;\n})();", + evaluatorType: "graaljs", + iteration: + "${" + + (task?.taskReferenceName + ? task?.taskReferenceName + : "do_while_ref") + + ".output}", + }, + }, + ] as unknown as TaskDef[], + }, +}); diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/DynamicForkOperatorForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/DynamicForkOperatorForm.tsx new file mode 100644 index 0000000000..0bd9ac853e --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/DynamicForkOperatorForm.tsx @@ -0,0 +1,73 @@ +import { Box, Grid } from "@mui/material"; +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import { ConductorFlatMapFormBase } from "components/v1/FlatMapForm/ConductorFlatMapForm"; +import { path as _path } from "lodash/fp"; +import { updateField } from "utils/fieldHelpers"; +import { Optional } from "./OptionalFieldForm"; +import TaskFormSection from "./TaskFormSection"; +import { TaskFormProps } from "./types"; + +const inputParametersPath = "inputParameters"; +const dynamicForkTasksParamPath = "dynamicForkTasksParam"; +const dynamicForkTasksInputParamNamePath = "dynamicForkTasksInputParamName"; + +export const DynamicForkOperatorForm = ({ task, onChange }: TaskFormProps) => ( + + + + + + onChange(updateField(inputParametersPath, value, task)) + } + /> + + + + + + + + Map parameters from above to tasks and inputs. + + + onChange(updateField(dynamicForkTasksParamPath, value, task)) + } + /> + + + + onChange( + updateField(dynamicForkTasksInputParamNamePath, value, task), + ) + } + /> + + + + + + + + + +); diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/DynamicOperatorForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/DynamicOperatorForm.tsx new file mode 100644 index 0000000000..7728bd60b7 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/DynamicOperatorForm.tsx @@ -0,0 +1,56 @@ +import { Box, Grid } from "@mui/material"; +import { path as _path } from "lodash/fp"; + +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import { ConductorFlatMapFormBase } from "components/v1/FlatMapForm/ConductorFlatMapForm"; +import { updateField } from "utils/fieldHelpers"; +import { Optional } from "./OptionalFieldForm"; +import TaskFormSection from "./TaskFormSection"; +import { TaskFormProps } from "./types"; + +const inputParametersPath = "inputParameters"; + +export const DynamicForkForm = ({ task, onChange }: TaskFormProps) => ( + + + + + + onChange(updateField(inputParametersPath, value, task)) + } + /> + + + + + onChange(updateField("dynamicTaskNameParam", changes, task)) + } + inputProps={{ + tooltip: { + title: "Dynamic Task to be Executed", + content: + "Indicates the name of the task, or the variable, to be called during workflow execution.", + }, + }} + /> + + + + + + + + + +); diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/EnforceSchemaForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/EnforceSchemaForm.tsx new file mode 100644 index 0000000000..e1512fe573 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/EnforceSchemaForm.tsx @@ -0,0 +1,55 @@ +import React from "react"; +import { Box, FormControlLabel, Link, Switch, Typography } from "@mui/material"; +interface EnforceSchemaProps { + onChange: (value: boolean) => void; + value?: boolean; + defaultValue?: boolean; + showEnforceSchemaSwitch?: boolean; +} + +export const EnforceSchema = ({ + onChange, + value, + defaultValue = false, + showEnforceSchemaSwitch = false, +}: EnforceSchemaProps) => { + const handleChange = (e: React.ChangeEvent) => { + onChange(e.target.checked); + }; + + return ( + <> + {showEnforceSchemaSwitch && ( + } + label={ + + Enforce Schema + + } + /> + )} + + + Select input and/or output schemas to validate task data and ensure + data integrity throughout the workflow execution. + + + {"Learn more."} + + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/EventTaskForm/EventTaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/EventTaskForm/EventTaskForm.tsx new file mode 100644 index 0000000000..f4f924bf23 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/EventTaskForm/EventTaskForm.tsx @@ -0,0 +1,82 @@ +import { Box, Grid, Switch } from "@mui/material"; +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import { ConductorFlatMapFormBase } from "components/v1/FlatMapForm/ConductorFlatMapForm"; +import { path as _path } from "lodash/fp"; +import { updateField } from "utils/fieldHelpers"; +import { useEventNameSuggestions } from "utils/hooks"; +import { Optional } from "../OptionalFieldForm"; +import TaskFormSection from "../TaskFormSection"; +import { TaskFormProps } from "../types"; + +const sinkPath = "sink"; +const inputParametersPath = "inputParameters"; +const asyncPath = "asyncComplete"; + +export const EventTaskForm = ({ task, onChange }: TaskFormProps) => { + return ( + + + + + + onChange(updateField(sinkPath, changes, task)) + } + /> + + + + + + + + + onChange(updateField(inputParametersPath, changes, task)) + } + /> + + + + + + + + + + + + + onChange(updateField(asyncPath, e.target.checked, task)) + } + /> + Async complete + + + When turned on, task completion occurs asynchronously, with the + task remaining in progress while waiting for external APIs or + events to complete the task. + + + + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/EventTaskForm/index.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/EventTaskForm/index.ts new file mode 100644 index 0000000000..81cc92bfc3 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/EventTaskForm/index.ts @@ -0,0 +1 @@ +export * from "./EventTaskForm"; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/FieldTypeDropdown.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/FieldTypeDropdown.tsx new file mode 100644 index 0000000000..9b3c7d643a --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/FieldTypeDropdown.tsx @@ -0,0 +1,75 @@ +import { FunctionComponent, ReactNode } from "react"; +import { MenuItem } from "@mui/material"; +import { + FIELD_TYPE_BOOLEAN, + FIELD_TYPE_NULL, + FIELD_TYPE_NUMBER, + FIELD_TYPE_OBJECT, + FIELD_TYPE_STRING, + FieldType, +} from "types/common"; +import { inferType } from "utils"; +import ConductorSelect from "components/v1/ConductorSelect"; + +const fieldTypeOption: FieldType[] = [ + FIELD_TYPE_STRING, + FIELD_TYPE_NUMBER, + FIELD_TYPE_BOOLEAN, + FIELD_TYPE_NULL, + FIELD_TYPE_OBJECT, +]; + +function getFieldTypeLabel(type: string): string { + switch (type) { + case FIELD_TYPE_NUMBER: + return "Number"; + case FIELD_TYPE_BOOLEAN: + return "Boolean"; + case FIELD_TYPE_NULL: + return "Null"; + case FIELD_TYPE_OBJECT: + return "Object/Array"; + default: + return "String"; + } +} +interface FieldTypeDropdownProps { + value: any; + onTypeChange: (value: FieldType) => void; + hideObjectArray?: boolean; + allowedTypes?: FieldType[]; + label?: ReactNode; +} + +const FieldTypeDropdown: FunctionComponent = ({ + value, + onTypeChange, + hideObjectArray, + allowedTypes = fieldTypeOption, + label, +}) => { + const filteredOptions = fieldTypeOption.filter( + (type) => + allowedTypes.includes(type) && + (!hideObjectArray || type !== FIELD_TYPE_OBJECT), + ); + + return ( + { + onTypeChange(ev.target.value as FieldType); + }} + > + {filteredOptions.map((type) => ( + + {getFieldTypeLabel(type)} + + ))} + + ); +}; + +export default FieldTypeDropdown; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/FlexibleAutoCompleteVariables.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/FlexibleAutoCompleteVariables.tsx new file mode 100644 index 0000000000..87d5bb881e --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/FlexibleAutoCompleteVariables.tsx @@ -0,0 +1,69 @@ +import { Grid, Box, Autocomplete } from "@mui/material"; +import { Input } from "components"; +import match from "autosuggest-highlight/match"; +import parse from "autosuggest-highlight/parse"; + +const FlexibleAutoCompleteVariables = ({ + options = [], + value = "", + onChange = (_newValues) => {}, + label = "", +}: { + options: Array; + value?: string; + onChange?: (newValues: string) => void; + label?: string; +}) => { + const handleChange = (e: any, data: string) => { + onChange(data); + }; + + const handleSelect = (e: any, data: string) => { + let result = ""; + if (data) { + result = value + data.toString(); + } + onChange(result); + }; + + return ( + + + + option} + renderOption={(props, option, { inputValue }) => { + const matches = match(option as string, inputValue); + const parts = parse(option as string, matches); + return ( +
  • + {parts.map((part, index) => ( + + {part.text} + + ))} +
  • + ); + }} + renderInput={(params) => } + value={value} + onChange={handleSelect} + onInputChange={handleChange} + /> +
    +
    +
    + ); +}; + +export default FlexibleAutoCompleteVariables; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/GRPCTaskForm/GRPCTaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/GRPCTaskForm/GRPCTaskForm.tsx new file mode 100644 index 0000000000..18ebfac3e8 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/GRPCTaskForm/GRPCTaskForm.tsx @@ -0,0 +1,535 @@ +import { Box, Grid, Checkbox, FormControlLabel } from "@mui/material"; +import { useContext, useEffect, useMemo, useState } from "react"; + +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; + +import { TaskType } from "types"; + +import { ConductorCacheOutput } from "../ConductorCacheOutputForm"; +import { MaybeVariable } from "../MaybeVariable"; +import { Optional } from "../OptionalFieldForm"; +import TaskFormSection from "../TaskFormSection"; + +import { GrpcTaskFormProps } from "./types"; + +import { useInterpret } from "@xstate/react"; +import { useAuthHeaders } from "utils/query"; +import { ConductorAutoComplete } from "components/v1"; +import { ServiceDefDto, ServiceType } from "types/RemoteServiceTypes"; +import { splitHostAndPort } from "utils/remoteServices"; + +import { serviceMethodsMachine } from "../HTTPTaskForm/state/machine"; +import { useServiceMethodsDefinition } from "../HTTPTaskForm/state/hook"; +import { updateField } from "utils/fieldHelpers"; +import { ConductorAdditionalHeaders } from "../HTTPTaskForm/ConductorAdditionalHeaders"; +import { SchemaDefinition } from "types/SchemaDefinition"; +import { ConductorCodeBlockInput } from "components/v1/ConductorCodeBlockInput"; +import { tryToJson } from "utils/utils"; +import { featureFlags, FEATURES } from "utils/flags"; +import HedgingConfigForm from "../HedgingConfigForm"; +import ServiceRegistryPopulator from "../ServiceRegistrySelector"; +import MuiTypography from "components/MuiTypography"; +import { Link } from "react-router"; +import EditTaskDefConfigModal from "../HTTPTaskForm/EditTaskDefConfigModal"; +import { MessageContext } from "components/v1/layout/MessageContext"; +import { HandleUpdateTemplateEvent } from "../HTTPTaskForm/state/types"; + +export const GRPCTaskForm = ({ task, onChange }: GrpcTaskFormProps) => { + const authHeaders = useAuthHeaders(); + const { setMessage } = useContext(MessageContext); + const currentTask = useMemo(() => task, [task]); + + const showServiceTemplateSelector = featureFlags.isEnabled( + FEATURES.REMOTE_SERVICES, + ); + + const serviceMethodsActor = useInterpret(serviceMethodsMachine, { + ...(process.env.NODE_ENV === "development" ? { devTools: true } : {}), + context: { + authHeaders, + currentTaskDefName: task?.name, + }, + actions: { + setErrorMessage: (_context, event: any) => { + setMessage({ + severity: "error", + text: event?.data?.message ?? "Something went wrong", + }); + }, + setSuccessMessage: (_context, event: any) => { + setMessage({ + severity: "success", + text: event?.data?.message ?? "Task definition updated successfully", + }); + }, + templateUpdate: ( + { selectedMethod, selectedService, selectedSchema }, + { url, headers }: HandleUpdateTemplateEvent, + ) => { + if (!selectedMethod) { + return; + } + + const method = ( + selectedSchema as unknown as ServiceDefDto + )?.methods?.find( + ({ methodName }) => methodName === selectedMethod?.methodName, + ); + + const { host: serviceUriHost, port: serviceUriPort } = splitHostAndPort( + selectedService?.serviceURI, + ); + const { host: selectedHost, port: selectedPort } = + splitHostAndPort(url); + const host = selectedHost ? selectedHost : serviceUriHost; + const port = selectedPort ? selectedPort : serviceUriPort; + const operationNamePlusMethodName = + (selectedMethod?.operationName ? selectedMethod?.operationName : "") + + "/" + + (selectedMethod?.methodName ? selectedMethod?.methodName : ""); + onChange({ + ...task, + inputParameters: { + ...task?.inputParameters, + service: selectedService?.name, + methodType: selectedMethod?.methodType, + method: operationNamePlusMethodName, + host: host, + port: port, + headers: headers, + request: method?.exampleInput ?? {}, + inputType: selectedMethod?.inputType, + outputType: selectedMethod?.outputType, + }, + }); + }, + }, + }); + const [errorInJsonField, setErrorInJsonField] = useState(false); + + const onChangeHttpRequestBody = (maybeEventOrValue: string) => { + const json = tryToJson(maybeEventOrValue); + if (json != null) { + onChange(updateField("inputParameters.request", json, task)); + setErrorInJsonField(false); + } else if (json == null && task?.inputParameters?.request != null) { + setErrorInJsonField(true); + } + }; + + const onChangeHeaders = (modHttpHeaders: any) => + onChange(updateField("inputParameters.headers", modHttpHeaders, task)); + + const [ + { + services, + selectedService, + selectedServiceMethods, + selectedMethod, + schemas, + showServiceRegistryPopulatorModal, + currentTaskDefinition, + selectedHost, + }, + { + handleSelectService, + handleSelectMethod, + handleShowServiceRegistryPopulatorModal, + handleChangeTaskDefName, + handleSelectHost, + }, + ] = useServiceMethodsDefinition(serviceMethodsActor); + + const selectedServiceConfig = useMemo( + () => + services?.find( + (item: Partial) => + item.name === task?.inputParameters?.service, + )?.config, + [services, task?.inputParameters?.service], + ); + + useEffect(() => { + if (showServiceTemplateSelector) { + handleChangeTaskDefName(task?.name); + } + }, [task?.name, handleChangeTaskDefName, showServiceTemplateSelector]); + + return ( + + onChange(updateField("inputParameters", val, task))} + path={"inputParameters"} + taskType={TaskType.GRPC} + > + {/* service selection section */} + + {showServiceTemplateSelector && ( + + )} + {/* end of service selection section */} + + + + + + + {task?.inputParameters?.service && ( + + + onChange( + updateField("inputParameters.service", val, task), + ) + } + label="Service" + disabled + /> + + )} + + + onChange( + updateField("inputParameters.method", val, task), + ) + } + /> + + + {showServiceTemplateSelector && + task?.inputParameters?.service && ( + + + Edit service definition + + + )} + + + + onChange(updateField("inputParameters.host", val, task)) + } + value={task?.inputParameters?.host ?? ""} + label="Host" + /> + + + + onChange(updateField("inputParameters.port", val, task)) + } + coerceTo="integer" + /> + + + + + + + onChange( + updateField("inputParameters.methodType", val, task), + ) + } + value={task?.inputParameters?.methodType ?? ""} + label="Method type" + /> + + + + onChange( + updateField( + "inputParameters.useSSL", + e.target.checked, + task, + ), + ) + } + /> + } + label="Use SSL" + /> + + + + onChange( + updateField( + "inputParameters.trustCert", + e.target.checked, + task, + ), + ) + } + /> + } + label="Trust Certificate" + /> + + + {showServiceTemplateSelector && ( + <> + {selectedServiceConfig && + selectedServiceConfig?.circuitBreakerConfig && ( + + + CircuitBreaker Configuration: + + + + + + {Object.entries( + selectedServiceConfig?.circuitBreakerConfig, + ).map(([key, value]) => ( + + {}} + value={value as string} + label={key} + /> + + ))} + + + + + + )} + {/* end of circuitBreakerConfig */} + + {currentTaskDefinition ? ( + + { + onChange( + updateField( + "inputParameters.hedgingConfig", + data, + task, + ), + ); + }} + /> + } + /> + + ) : ( + + { + onChange( + updateField( + "inputParameters.hedgingConfig", + data, + task, + ), + ); + }} + /> + + )} + + )} + + + + + + + onChange( + updateField("inputParameters.inputType", val, task), + ) + } + otherOptions={schemas?.map( + (item: SchemaDefinition) => item?.name, + )} + label="Input type" + /> + + + item?.name, + )} + onChange={(val) => + onChange( + updateField("inputParameters.outputType", val, task), + ) + } + /> + + + + + + + + + + + + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/GRPCTaskForm/index.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/GRPCTaskForm/index.ts new file mode 100644 index 0000000000..a911fd93e9 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/GRPCTaskForm/index.ts @@ -0,0 +1 @@ +export * from "./GRPCTaskForm"; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/GRPCTaskForm/types.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/GRPCTaskForm/types.ts new file mode 100644 index 0000000000..b23732922d --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/GRPCTaskForm/types.ts @@ -0,0 +1,6 @@ +import { TaskFormProps } from "../types"; +import { GrpcTaskDef } from "types"; + +export interface GrpcTaskFormProps extends TaskFormProps { + task: GrpcTaskDef; +} diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/GetDocumentTaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/GetDocumentTaskForm.tsx new file mode 100644 index 0000000000..ddfd158a1c --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/GetDocumentTaskForm.tsx @@ -0,0 +1,43 @@ +import { Box } from "@mui/material"; +import { LLMFormFields } from "pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields"; +import { UiIntegrationsFieldType } from "types"; +import { fieldsToFieldsFieldsComponents } from "utils/fieldHelpers"; +import { ConductorCacheOutput } from "./ConductorCacheOutputForm"; +import TaskFormSection from "./TaskFormSection"; +import { TaskFormProps } from "./types"; +import LLMFormFieldsWrapper from "./LLMFormFields/LLMFormFieldsWrapper"; + +const fields = [ + UiIntegrationsFieldType.URL, + UiIntegrationsFieldType.MEDIA_TYPE, +]; +const fieldFieldComponents = fieldsToFieldsFieldsComponents(fields); + +export const GetDocumentTaskForm = ({ task, onChange }: TaskFormProps) => { + return ( + + {(actor) => ( + + + + + + + + + )} + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/GetSignedJwtForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/GetSignedJwtForm.tsx new file mode 100644 index 0000000000..52b33e6833 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/GetSignedJwtForm.tsx @@ -0,0 +1,166 @@ +import { Box, Grid } from "@mui/material"; +import { ConductorAutoComplete } from "components/v1"; +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import { GetSignedJWTAlgorithmType } from "types"; +import { ConductorCacheOutput } from "./ConductorCacheOutputForm"; +import { ConductorValueInput } from "./ConductorValueInput"; +import { Optional } from "./OptionalFieldForm"; +import TaskFormSection from "./TaskFormSection"; +import { TaskFormProps } from "./types"; +import { useGetSetHandler } from "./useGetSetHandler"; + +const DEFAULT_VALUES_FOR_ARRAY = { object: [] }; +const SUBJECT_PATH = "inputParameters.subject"; +const ISSUER_PATH = "inputParameters.issuer"; +const PRIVATE_KEY_PATH = "inputParameters.privateKey"; +const PRIVATE_KEY_ID_PATH = "inputParameters.privateKeyId"; +const AUDIENCE_PATH = "inputParameters.audience"; +const TTL_PATH = "inputParameters.ttlInSecond"; +const SCOPES_PATH = "inputParameters.scopes"; +const ALGORITHM_PATH = "inputParameters.algorithm"; + +const GetSignedJwtForm = (props: TaskFormProps) => { + const { task, onChange } = props; + + const [subject, handleSubject] = useGetSetHandler(props, SUBJECT_PATH); + const [issuer, handleIssuer] = useGetSetHandler(props, ISSUER_PATH); + const [privateKey, handlePrivateKey] = useGetSetHandler( + props, + PRIVATE_KEY_PATH, + ); + const [privateKeyId, handlePrivateKeyId] = useGetSetHandler( + props, + PRIVATE_KEY_ID_PATH, + ); + const [audience, handleAudience] = useGetSetHandler(props, AUDIENCE_PATH); + const [ttl, handleTtl] = useGetSetHandler(props, TTL_PATH); + const [scopes, handleScopes] = useGetSetHandler(props, SCOPES_PATH); + const [algorithm, handleAlgorithm] = useGetSetHandler(props, ALGORITHM_PATH); + + return ( + + + + + + + + + + + + + + + + + + + + + + + { + handleScopes(val); + }} + defaultObjectValue={DEFAULT_VALUES_FOR_ARRAY} + /> + + + + val !== null && handleAlgorithm(val) + } + value={algorithm} + clearIcon={false} + /> + + + + + + + + + + + ); +}; +export default GetSignedJwtForm; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/GetWorkflowTaskForm/GetWorkflowTaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/GetWorkflowTaskForm/GetWorkflowTaskForm.tsx new file mode 100644 index 0000000000..d907125f29 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/GetWorkflowTaskForm/GetWorkflowTaskForm.tsx @@ -0,0 +1,54 @@ +import { Box, FormControlLabel, Grid } from "@mui/material"; +import MuiCheckbox from "components/MuiCheckbox"; +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import { updateField } from "utils/fieldHelpers"; +import { Optional } from "../OptionalFieldForm"; +import TaskFormSection from "../TaskFormSection"; +import { TaskFormProps } from "../types"; + +const GET_WORKFLOW_ID_PATH = "inputParameters.id"; +const GET_WORKFLOW_INCLUDE_PATH = "inputParameters.includeTasks"; + +export const GetWorkflowTaskForm = ({ task, onChange }: TaskFormProps) => { + return ( + + + + + + onChange(updateField(GET_WORKFLOW_ID_PATH, value, task)) + } + /> + + + + onChange( + updateField( + GET_WORKFLOW_INCLUDE_PATH, + event.target.checked, + task, + ), + ) + } + /> + } + label="Include tasks" + /> + + + + + + + + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/GetWorkflowTaskForm/index.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/GetWorkflowTaskForm/index.ts new file mode 100644 index 0000000000..afd77ec129 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/GetWorkflowTaskForm/index.ts @@ -0,0 +1 @@ +export * from "./GetWorkflowTaskForm"; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/ConductorAdditionalHeaders.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/ConductorAdditionalHeaders.tsx new file mode 100644 index 0000000000..9aaedd82c8 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/ConductorAdditionalHeaders.tsx @@ -0,0 +1,227 @@ +import { Box, Grid, IconButton } from "@mui/material"; +import _difference from "lodash/difference"; +import _first from "lodash/first"; +import _isEmpty from "lodash/isEmpty"; +import _isNil from "lodash/isNil"; +import _last from "lodash/last"; +import { FunctionComponent, useMemo, useState } from "react"; + +import { Button } from "components"; +import { ConductorAutoComplete } from "components/v1"; +import { ConductorEmptyGroupField } from "components/v1/ConductorEmptyGroupField"; +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import AddIcon from "components/v1/icons/AddIcon"; +import TrashIcon from "components/v1/icons/TrashIcon"; +import { HEADER_SUGGESTIONS } from "utils/constants/httpSuggestions"; +import { OBJECT_PROPERTY_NAME_REGEX } from "utils/regex"; +import maybeVariable from "../maybeVariableHOC"; + +interface HeaderFieldProps { + availableOptions: string[]; + onChange: (key: string, value: string) => void; + headerKey?: string; + value?: string; + onRemove?: (key: string) => void; + autoFocus?: boolean; + existingKeys: string[]; +} + +const HeaderField: FunctionComponent = ({ + availableOptions, + onChange, + headerKey, + value, + onRemove, + existingKeys, +}) => { + const [keyValuePair, setKeyValuePair] = useState< + [string | undefined, string | undefined] + >([headerKey, value]); + const [isDuplicatedKey, setIsDuplicatedKey] = useState(false); + const [invalidKey, setInvalidKey] = useState(false); + const [errorMessage, setErrorMessage] = useState<{ + [key: string]: string; + }>({}); + + const handleSetKeyValuePair = (key?: string, value?: string) => { + const newValue: [string | undefined, string | undefined] = [ + key && key !== "null" ? key : "", + value, + ]; + + setKeyValuePair(newValue); + if (key && !newValue.some(_isNil)) { + onChange(key, value!); + } + }; + + const handleDropdownInputChange = (value: string) => { + const invalid = !OBJECT_PROPERTY_NAME_REGEX.test(value); + const duplicated = + _first(keyValuePair) === "" && + existingKeys.filter((key) => key === value).length >= 1; + + setErrorMessage((prevState) => { + const previousState = { ...prevState }; + if (invalid) { + previousState.invalid = + "Key can only contain letters, numbers, and the following special characters: !#$%&'*+-.^_`|~"; + } else { + delete previousState.invalid; + } + + if (duplicated) { + previousState.duplicated = "Key is duplicated"; + } else { + delete previousState.duplicated; + } + + return previousState; + }); + + setInvalidKey(invalid); + setIsDuplicatedKey(duplicated); + }; + + return ( + + + + { + handleSetKeyValuePair(selectedKey, _last(keyValuePair)); + }} + onBlur={(event: any) => { + if (!invalidKey && !isDuplicatedKey) { + handleSetKeyValuePair(event.target.value, _last(keyValuePair)); + } + }} + onTextInputChange={handleDropdownInputChange} + error={isDuplicatedKey || invalidKey} + helperText={Object.values(errorMessage).join("\n")} + /> + + + + handleSetKeyValuePair(_first(keyValuePair), val) + } + placeholder={_isEmpty(value) ? "New value" : ""} + value={_last(keyValuePair)} + /> + + + + {onRemove && ( + onRemove!(headerKey!)} + style={{ paddingTop: "0.42em" }} + > + + + )} + + + ); +}; + +interface ConductorAdditionalHeadersProps { + headers: Record; + onChangeHeaders: (headers: Record) => void; +} + +const ConductorAdditionalHeadersBase: FunctionComponent< + ConductorAdditionalHeadersProps +> = ({ headers = {}, onChangeHeaders }) => { + const [valueEntries, entryKeys] = useMemo(() => { + const entries = Object.entries(headers); + const entryKeys = Object.keys(headers); + return [entries, entryKeys]; + }, [headers]); + const handleAddChangeItem = ( + headerKey: string, + headerValue: string, + idx: number, + ) => { + const modifiedPreservingOrder = + idx === entryKeys.length + ? Object.assign({}, headers, { [headerKey]: headerValue }) + : Object.fromEntries( + valueEntries.map(([key, val], innerIdx) => + innerIdx === idx ? [headerKey, headerValue] : [key, val], + ), + ); + + onChangeHeaders(modifiedPreservingOrder); + }; + + const handleRemoveItem = (key: string) => { + const valueClone = { ...headers }; + delete valueClone[key]; + onChangeHeaders(valueClone); + }; + + return valueEntries.length === 0 ? ( + handleAddChangeItem("", "", entryKeys.length)} + /> + ) : ( + <> + + + {valueEntries.map(([key, value], idx) => ( + option !== "")} + key={`${idx}_${valueEntries.length}`} + headerKey={key} + value={value} + onChange={(changedKey, changedValue) => + handleAddChangeItem(changedKey, changedValue, idx) + } + onRemove={handleRemoveItem} + existingKeys={valueEntries.map((entry) => _first(entry)!)} + /> + ))} + + + + + + ); +}; + +const ConductorAdditionalHeaders = maybeVariable( + ConductorAdditionalHeadersBase, +); +export { ConductorAdditionalHeaders, ConductorAdditionalHeadersBase }; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/EditTaskDefConfigModal.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/EditTaskDefConfigModal.tsx new file mode 100644 index 0000000000..f4d9481f48 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/EditTaskDefConfigModal.tsx @@ -0,0 +1,225 @@ +import { Box, Button, Grid, Stack } from "@mui/material"; +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import React, { ReactElement, useState } from "react"; +import { useServiceMethodsDefinition } from "./state/hook"; +import { ActorRef } from "xstate"; +import { ServiceMethodsMachineEvents } from "./state/types"; +import UIModal from "components/UIModal"; +import { Edit } from "@mui/icons-material"; +import ConductorInputNumber from "components/v1/ConductorInputNumber"; +import MuiTypography from "components/MuiTypography"; + +import XCloseIcon from "components/v1/icons/XCloseIcon"; +import SaveIcon from "components/v1/icons/SaveIcon"; +import { NotePencil } from "@phosphor-icons/react"; + +const EditTaskDefConfigModal = ({ + actor, + hedgingComponent, +}: { + actor: ActorRef; + hedgingComponent: ReactElement; +}) => { + const [show, setShow] = useState(false); + const [ + { currentTaskDefinition }, + { + handleUpdateTaskConfig, + handleChangeTaskConfig, + handleResetModifiedTaskConfig, + }, + ] = useServiceMethodsDefinition(actor); + + const handleShow = (val: boolean) => { + handleResetModifiedTaskConfig(); + setShow(val); + }; + + return ( + <> + + + {hedgingComponent} + + + + Retry settings: + + + handleShow(true)} + size={16} + cursor={"pointer"} + style={{ marginLeft: "5px", color: "#767676" }} + /> + + + + + {}} + value={currentTaskDefinition?.retryCount} + label={"Retry count"} + /> + + + + + + + + + + Rate limit settings: + + handleShow(true)} + size={16} + cursor={"pointer"} + style={{ marginLeft: "5px", color: "#767676" }} + /> + + + + + + + {}} + value={currentTaskDefinition?.rateLimitPerFrequency} + label={"Rate limit per frequency"} + /> + + + {}} + value={currentTaskDefinition?.rateLimitFrequencyInSeconds} + label={"Frequency seconds"} + /> + + + + + + + } + description={`Edit retry and rate limit settings for '${currentTaskDefinition?.name}' task`} + enableCloseButton + > + + + + {}} + value={currentTaskDefinition?.name} + label={"Task name"} + /> + + {/* rate limit settings */} + + + Rate limit settings + + + + + + + handleChangeTaskConfig("rateLimitPerFrequency", value) + } + value={currentTaskDefinition?.rateLimitPerFrequency} + label={"Rate limit per frequency"} + inputProps={{ + allowNegative: false, + }} + tooltip={{ + title: "Rate limit per frequency", + content: + "The number of task executions given to workers per frequency window.", + }} + /> + + + + handleChangeTaskConfig( + "rateLimitFrequencyInSeconds", + value, + ) + } + value={currentTaskDefinition?.rateLimitFrequencyInSeconds} + label={"Frequency seconds"} + inputProps={{ + allowNegative: false, + }} + tooltip={{ + title: "Frequency seconds", + content: + "The duration of the frequency window in seconds.", + }} + /> + + + + + {/* retry settings */} + + + Retry settings + + + + + handleChangeTaskConfig("retryCount", value) + } + value={currentTaskDefinition?.retryCount} + label={"Retry count"} + inputProps={{ + allowNegative: false, + }} + /> + + + + + + + + + + ); +}; + +export default EditTaskDefConfigModal; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/Encode.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/Encode.tsx new file mode 100644 index 0000000000..0827938339 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/Encode.tsx @@ -0,0 +1,33 @@ +import React, { FunctionComponent } from "react"; +import { Box, Switch } from "@mui/material"; + +interface EncodeProps { + onChange: (value: boolean) => void; + value: boolean; + title?: string; +} + +export const Encode: FunctionComponent = ({ + onChange, + value, + title = "Encode", +}) => { + const handleChange = (e: React.ChangeEvent) => { + onChange(e.target.checked); + }; + + return ( + <> + + + + {title} + + + Automatically encodes query parameters in the URI before sending the + HTTP request. + + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/HTTPPollTaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/HTTPPollTaskForm.tsx new file mode 100644 index 0000000000..ba8c04c677 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/HTTPPollTaskForm.tsx @@ -0,0 +1,405 @@ +import { Box, FormControlLabel, Grid } from "@mui/material"; +import { useInterpret } from "@xstate/react"; +import RadioButtonGroup from "components/RadioButtonGroup"; +import { ConductorCodeBlockInput } from "components/v1/ConductorCodeBlockInput"; +import ConductorInput from "components/v1/ConductorInput"; +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import { ConductorFlatMapFormBase } from "components/v1/FlatMapForm/ConductorFlatMapForm"; +import { assoc as _assoc, path as _path, pipe as _pipe } from "lodash/fp"; +import { mock } from "mock-json-schema"; +import { ServiceType } from "types/RemoteServiceTypes"; +import { useMemo, useState } from "react"; +import { colors } from "theme/tokens/variables"; +import { HTTPMethods, PollingStrategy, TaskType } from "types"; +import { + CONTENT_TYPE_SUGGESTIONS, + HEADERS_PATH, + HTTP_REQUEST_PATH, +} from "utils/constants/httpSuggestions"; +import { updateField } from "utils/fieldHelpers"; +import { featureFlags, FEATURES } from "utils/flags"; +import { useAuthHeaders } from "utils/query"; +import { getBaseUrl } from "utils/utils"; +import { ConductorCacheOutput } from "../ConductorCacheOutputForm"; +import { MaybeVariable } from "../MaybeVariable"; +import { Optional } from "../OptionalFieldForm"; +import ServiceRegistryPopulator from "../ServiceRegistrySelector"; +import TaskFormSection from "../TaskFormSection"; +import { ConductorAdditionalHeaders } from "./ConductorAdditionalHeaders"; +import { Encode } from "./Encode"; +import { useCreateHttpRequestHandlers } from "./common"; +import { useServiceMethodsDefinition } from "./state/hook"; +import { serviceMethodsMachine } from "./state/machine"; +import { HandleUpdateTemplateEvent } from "./state/types"; +import { HttpTaskFormProps } from "./types"; + +const httpRequestUriPath = "inputParameters.http_request.uri"; +const httpRequestTerminationConditionPath = + "inputParameters.http_request.terminationCondition"; +const httpRequestPollingIntervalPath = + "inputParameters.http_request.pollingInterval"; + +export const HTTPPollTaskForm = ({ task, onChange }: HttpTaskFormProps) => { + const authHeaders = useAuthHeaders(); + const currentTask = useMemo(() => task, [task]); + const [ + { + onChangeHttpRequest, + onChangeHttpRequestBody, + onChangeMethod, + onChangeAccept, + onChangeContentType, + onChangeHeaders, + onChangePollingStrategy, + onChangeEncode, + generatePath, + onChangeHttpRequestBodyParameter, + }, + { + httpHeaders, + httpRequestBody, + accept, + contentType, + method, + pollingStrategy, + httpRequestEncode, + errorInJsonField, + }, + ] = useCreateHttpRequestHandlers({ task, onChange }); + + const showServiceTemplateSelector = featureFlags.isEnabled( + FEATURES.REMOTE_SERVICES, + ); + + const serviceMethodsActor = useInterpret(serviceMethodsMachine, { + ...(process.env.NODE_ENV === "development" ? { devTools: true } : {}), + context: { + authHeaders, + }, + actions: { + templateUpdate: ( + { selectedMethod, selectedSchema, selectedService }, + { url, headers }: HandleUpdateTemplateEvent, + ) => { + if (!selectedMethod) { + return; + } + const schema = selectedSchema?.data ?? {}; + const generatedPayload = mock(schema); + const payloadBody = generatedPayload ? generatedPayload : {}; + const baseUrl = selectedHost + ? selectedHost + : getBaseUrl(selectedService?.serviceURI); + onChange({ + ...task, + inputParameters: { + ...task?.inputParameters, + http_request: { + ...task?.inputParameters?.http_request, + uri: baseUrl + url, + method: selectedMethod?.methodType, + body: payloadBody, + headers: headers, + accept: selectedMethod?.responseContentType ?? "application/json", + contentType: + selectedMethod?.requestContentType ?? "application/json", + }, + }, + taskDefinition: { + ...task?.taskDefinition, + inputSchema: { + ...(selectedMethod?.inputType + ? { + name: selectedMethod?.inputType, + type: "JSON", + } + : {}), + }, + outputSchema: { + ...(selectedMethod?.outputType + ? { + name: selectedMethod?.outputType, + type: "JSON", + } + : {}), + }, + }, + }); + }, + }, + }); + + const [ + { + services, + selectedService, + selectedServiceMethods, + selectedMethod, + showServiceRegistryPopulatorModal, + selectedHost, + }, + { + handleSelectService, + handleSelectMethod, + handleShowServiceRegistryPopulatorModal, + handleSelectHost, + }, + ] = useServiceMethodsDefinition(serviceMethodsActor); + + const [bodyViewType, setBodyViewType] = useState("JSON"); + + return ( + + + {/* service selection section */} + {showServiceTemplateSelector && ( + + )} + {/* end of service selection section */} + + + + + + + + + + onChange( + updateField(httpRequestUriPath, value, currentTask), + ) + } + /> + + + + + + + + + + + + + + + + + + onChange( + updateField( + httpRequestTerminationConditionPath, + value, + currentTask, + ), + ) + } + /> + + + + onChange( + updateField( + httpRequestPollingIntervalPath, + value, + currentTask, + ), + ) + } + /> + + + + + + + + + + + + + + + + + + + + + { + setBodyViewType(value); + }} + /> + } + label="Body:" + sx={{ + marginBottom: 2, + marginLeft: 0, + "& .MuiFormControlLabel-label": { + fontWeight: 600, + color: colors.gray07, + }, + }} + /> + {bodyViewType === "JSON" ? ( + + ) : ( + + onChangeHttpRequestBodyParameter(changes) + } + /> + )} + + + + + + + + + + + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/HTTPTaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/HTTPTaskForm.tsx new file mode 100644 index 0000000000..728d1eed0c --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/HTTPTaskForm.tsx @@ -0,0 +1,528 @@ +import { Box, Grid, Switch } from "@mui/material"; +import { useInterpret } from "@xstate/react"; +import RadioButtonGroup from "components/RadioButtonGroup"; +import { ConductorCodeBlockInput } from "components/v1/ConductorCodeBlockInput"; +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import { ConductorFlatMapFormBase } from "components/v1/FlatMapForm/ConductorFlatMapForm"; +import { MessageContext } from "components/v1/layout/MessageContext"; +import { assoc as _assoc, pipe as _pipe } from "lodash/fp"; +import { mock } from "mock-json-schema"; +import { ServiceType, ServiceDefDto } from "types/RemoteServiceTypes"; +import { useContext, useEffect, useMemo, useState } from "react"; +import { Link } from "react-router"; +import { HTTPMethods, TaskType } from "types"; +import { + CONTENT_TYPE_SUGGESTIONS, + HEADERS_PATH, +} from "utils/constants/httpSuggestions"; +import { featureFlags, FEATURES } from "utils/flags"; +import { useAuthHeaders } from "utils/query"; +import { getBaseUrl } from "utils/utils"; +import { ConductorCacheOutput } from "../ConductorCacheOutputForm"; +import HedgingConfigForm from "../HedgingConfigForm"; +import { MaybeVariable } from "../MaybeVariable"; +import { Optional } from "../OptionalFieldForm"; +import ServiceRegistryPopulator from "../ServiceRegistrySelector"; +import { TaskFormHeaderEventTypes } from "../TaskFormHeader/state"; +import TaskFormSection from "../TaskFormSection"; +import { ConductorAdditionalHeaders } from "./ConductorAdditionalHeaders"; +import EditTaskDefConfigModal from "./EditTaskDefConfigModal"; +import { Encode } from "./Encode"; +import { useCreateHttpRequestHandlers } from "./common"; +import { useServiceMethodsDefinition } from "./state/hook"; +import { serviceMethodsMachine } from "./state/machine"; +import { HandleUpdateTemplateEvent } from "./state/types"; +import { HttpTaskFormProps } from "./types"; + +export const HTTPTaskForm = ({ + task, + onChange, + taskFormHeaderActor, +}: HttpTaskFormProps) => { + const authHeaders = useAuthHeaders(); + const { setMessage } = useContext(MessageContext); + const currentTask = useMemo(() => task, [task]); + const [ + { + onChangeHttpRequest, + onChangeMethod, + onChangeService, + onChangeAccept, + onChangeContentType, + onChangeHeaders, + onChangeUri, + onChangeAsyncComplete, + onChangeHttpRequestBody, + onChangeEncode, + generatePath, + onChangeHttpRequestBodyParameter, + onChangeHedgingConfig, + }, + { + httpHeaders, + accept, + contentType, + method, + uri, + service, + httpRequestBody, + HTTP_REQUEST_PATH, + httpRequestEncode, + errorInJsonField, + hedgingConfig, + }, + ] = useCreateHttpRequestHandlers({ task, onChange }); + + const showServiceTemplateSelector = featureFlags.isEnabled( + FEATURES.REMOTE_SERVICES, + ); + + const serviceMethodsActor = useInterpret(serviceMethodsMachine, { + ...(process.env.NODE_ENV === "development" ? { devTools: true } : {}), + context: { + authHeaders, + currentTaskDefName: task?.name, + }, + actions: { + setErrorMessage: (_context, event: any) => { + setMessage({ + severity: "error", + text: event?.data?.message ?? "Something went wrong", + }); + }, + setSuccessMessage: (_context, event: any) => { + setMessage({ + severity: "success", + text: event?.data?.message ?? "Task definition updated successfully", + }); + }, + templateUpdate: ( + { selectedMethod, selectedSchema, selectedService }, + { url, headers }: HandleUpdateTemplateEvent, + ) => { + if (!selectedMethod) { + return; + } + const schema = selectedSchema?.data ?? {}; + const generatedPayload = mock(schema); + const payloadBody = generatedPayload ? generatedPayload : {}; + const baseUrl = selectedHost + ? selectedHost + : getBaseUrl(selectedService?.serviceURI); + onChange({ + ...task, + inputParameters: { + ...task?.inputParameters, + service: selectedService?.name, + uri: baseUrl + url, + method: selectedMethod?.methodType, + body: payloadBody, + headers: headers, + accept: selectedMethod?.responseContentType ?? "application/json", + contentType: + selectedMethod?.requestContentType ?? "application/json", + }, + taskDefinition: { + ...task?.taskDefinition, + inputSchema: { + ...(selectedMethod?.inputType + ? { + name: selectedMethod?.inputType, + type: "JSON", + } + : {}), + }, + outputSchema: { + ...(selectedMethod?.outputType + ? { + name: selectedMethod?.outputType, + type: "JSON", + } + : {}), + }, + }, + }); + }, + }, + }); + + const [ + { + services, + selectedService, + selectedServiceMethods, + selectedMethod, + showServiceRegistryPopulatorModal, + currentTaskDefinition, + selectedHost, + }, + { + handleSelectService, + handleSelectMethod, + handleShowServiceRegistryPopulatorModal, + handleChangeTaskDefName, + fetchTaskDefinition, + handleSelectHost, + }, + ] = useServiceMethodsDefinition(serviceMethodsActor); + + const [bodyViewType, setBodyViewType] = useState("JSON"); + + const selectedServiceConfig = useMemo( + () => + services?.find((item: Partial) => item.name === service) + ?.config, + [services, service], + ); + + useEffect(() => { + if (showServiceTemplateSelector) { + handleChangeTaskDefName(task?.name); + } + }, [task?.name, handleChangeTaskDefName, showServiceTemplateSelector]); + + useEffect(() => { + if (taskFormHeaderActor) { + const subscription = taskFormHeaderActor.subscribe((state) => { + if ( + state.event.type === + TaskFormHeaderEventTypes.TASK_CREATED_SUCCESSFULLY + ) { + fetchTaskDefinition(); + } + }); + + return () => { + subscription.unsubscribe(); + }; + } + }, [taskFormHeaderActor, fetchTaskDefinition]); + + return ( + + + {/* service selection section */} + {showServiceTemplateSelector && ( + + )} + {/* end of service selection section */} + + + + + {showServiceTemplateSelector && service && ( + + + + )} + + + + {(!showServiceTemplateSelector || !service) && ( + + + + )} + + + {showServiceTemplateSelector && service && ( + + + Edit service definition + + + )} + {showServiceTemplateSelector && service && ( + + + + + + + + )} + + + + + + + + + + + + + + + + + {showServiceTemplateSelector && ( + <> + {/* circuitBreakerConfig */} + {selectedServiceConfig && + selectedServiceConfig?.circuitBreakerConfig && ( + + + + + + {Object.entries( + selectedServiceConfig?.circuitBreakerConfig, + ).map(([key, value]) => ( + + {}} + value={value as string} + label={key} + /> + + ))} + + + + + + )} + + {currentTaskDefinition ? ( + + + } + /> + + ) : ( + + + + + + + + + + )} + + )} + + + + + + + + + + + + { + setBodyViewType(value); + }} + /> + {bodyViewType === "JSON" ? ( + + ) : ( + + onChangeHttpRequestBodyParameter(changes) + } + /> + )} + + + + + + + + + + + + Async complete + + + When turned on, task completion occurs asynchronously, with the + task remaining in progress while waiting for external APIs or + events to complete the task. + + + + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/common.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/common.ts new file mode 100644 index 0000000000..9b80f98829 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/common.ts @@ -0,0 +1,156 @@ +import _clone from "lodash/clone"; +import _path from "lodash/fp/path"; +import _get from "lodash/get"; +import { ChangeEvent, useState } from "react"; +import { HttpInputParameters } from "types"; +import { + ACCEPT_PATH, + CONTENT_TYPE_PATH, + HEADERS_PATH, + HEDGING_CONFIG_PATH, + HTTP_REQUEST_BODY, + HTTP_REQUEST_ENCODE, + METHOD_PATH, + POLLING_STRATEGY_PATH, + SERVICE_PATH, + URI_PATH, +} from "utils/constants/httpSuggestions"; +import { updateField } from "utils/fieldHelpers"; +import { tryToJson } from "utils/utils"; +import { GrpcTaskFormProps } from "../GRPCTaskForm/types"; +import { HttpTaskFormProps } from "./types"; + +export const useCreateHttpRequestHandlers = ({ + onChange, + task, +}: HttpTaskFormProps | GrpcTaskFormProps) => { + const generatePath = (path: any) => { + if (_path("inputParameters.http_request", task)) { + return `inputParameters.http_request.${path}`; + } else { + return `inputParameters.${path}`; + } + }; + + const onChangeHttpRequest = (request: string | HttpInputParameters) => + onChange(updateField("inputParameters.http_request", request, task)); + + const onChangeMethod = (method: string) => + onChange(updateField(generatePath(METHOD_PATH), method, task)); + + const onChangeHedgingConfig = (hedgingConfig: Record) => { + onChange( + updateField(generatePath(HEDGING_CONFIG_PATH), hedgingConfig, task), + ); + }; + + const onChangeAccept = (accept: string) => + onChange(updateField(generatePath(ACCEPT_PATH), accept, task)); + + const onChangeContentType = (contentType: string) => + onChange(updateField(generatePath(CONTENT_TYPE_PATH), contentType, task)); + + const onChangeHeaders = (modHttpHeaders: any) => + onChange(updateField(generatePath(HEADERS_PATH), modHttpHeaders, task)); + + const onChangePollingStrategy = (pollingStrategy: string) => + onChange( + updateField(generatePath(POLLING_STRATEGY_PATH), pollingStrategy, task), + ); + + const onChangeUri = (uri: string) => { + onChange(updateField(generatePath(URI_PATH), uri, task)); + }; + + const onChangeAsyncComplete = (event: ChangeEvent) => { + onChange(updateField("asyncComplete", event.target.checked, task)); + }; + + const onChangeOptional = (event: ChangeEvent) => { + onChange(updateField("optional", event.target.checked, task)); + }; + + const onChangeEncode = (value: boolean) => { + onChange(updateField(generatePath(HTTP_REQUEST_ENCODE), value, task)); + }; + + const onChangeService = (value: string) => + onChange(updateField(generatePath(SERVICE_PATH), value, task)); + + const [errorInJsonField, setErrorInJsonField] = useState(false); + + const onChangeHttpRequestBody = (maybeEventOrValue: string | any) => { + const json = tryToJson(maybeEventOrValue); + if (json != null) { + onChange(updateField(generatePath(HTTP_REQUEST_BODY), json, task)); + setErrorInJsonField(false); + } else if (json == null && httpRequestBody != null) { + setErrorInJsonField(true); + } + }; + + const onChangeHttpRequestBodyParameter = (maybeEventOrValue: any) => { + let newValue; + if (maybeEventOrValue?.nativeEvent) { + newValue = maybeEventOrValue.target.value; + } else if (maybeEventOrValue) { + newValue = maybeEventOrValue; + } + onChange(updateField(generatePath(HTTP_REQUEST_BODY), newValue, task)); + }; + + const httpHeaders = _clone(_get(task, generatePath(HEADERS_PATH), {})); + const accept = _clone(_get(task, generatePath(ACCEPT_PATH))); + const contentType = _clone(_get(task, generatePath(CONTENT_TYPE_PATH))); + const method = _clone(_get(task, generatePath(METHOD_PATH))); + const hedgingConfig = _clone(_get(task, generatePath(HEDGING_CONFIG_PATH))); + const service = _clone(_get(task, generatePath(SERVICE_PATH))); + const pollingStrategy = _clone( + _get(task, generatePath(POLLING_STRATEGY_PATH)), + ); + + const uri = _clone(_get(task, generatePath(URI_PATH))); + const httpRequestBody = _clone(_get(task, generatePath(HTTP_REQUEST_BODY))); + + const HTTP_REQUEST_PATH = _path("inputParameters.http_request", task) + ? "inputParameters.http_request" + : "inputParameters"; + + const httpRequestEncode = _clone( + _get(task, generatePath(HTTP_REQUEST_ENCODE)), + ); + + return [ + { + onChangeHttpRequest, + onChangeMethod, + onChangeAccept, + onChangeService, + onChangeContentType, + onChangeHeaders, + onChangePollingStrategy, + onChangeUri, + onChangeAsyncComplete, + onChangeOptional, + onChangeHttpRequestBody, + onChangeEncode, + generatePath, + onChangeHttpRequestBodyParameter, + onChangeHedgingConfig, + }, + { + httpHeaders, + accept, + contentType, + method, + pollingStrategy, + uri, + httpRequestBody, + HTTP_REQUEST_PATH, + httpRequestEncode, + errorInJsonField, + hedgingConfig, + service, + }, + ] as const; +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/index.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/index.ts new file mode 100644 index 0000000000..e03bc55b9e --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/index.ts @@ -0,0 +1,2 @@ +export * from "./HTTPTaskForm"; +export * from "./HTTPPollTaskForm"; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/state/actions.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/state/actions.ts new file mode 100644 index 0000000000..94f372ca49 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/state/actions.ts @@ -0,0 +1,93 @@ +import { assign, DoneInvokeEvent } from "xstate"; +import { + HandleChangeTaskConfigEvent, + SelectHostEvent, + SelectTaskEvent, + ServiceMethodsMachineContext, +} from "./types"; + +export const persistSelectedService = assign< + ServiceMethodsMachineContext, + DoneInvokeEvent +>((_context, { data }) => { + return { + selectedService: data, + }; +}); + +export const persistSelectedHost = assign< + ServiceMethodsMachineContext, + SelectHostEvent +>((_context, { data }) => { + return { + selectedHost: data, + }; +}); + +export const persistServices = assign< + ServiceMethodsMachineContext, + DoneInvokeEvent +>((_context, { data }) => { + return { + services: data, + }; +}); + +export const persistSelectedMethod = assign< + ServiceMethodsMachineContext, + DoneInvokeEvent +>((_context, { data }) => { + return { + selectedMethod: data, + }; +}); + +export const persistSelectedSchema = assign< + ServiceMethodsMachineContext, + DoneInvokeEvent +>((_context, { data }) => { + return { + selectedSchema: data, + }; +}); + +export const persistCurrentTaskDefName = assign< + ServiceMethodsMachineContext, + SelectTaskEvent +>((_context, { taskDefName }) => { + return { + currentTaskDefName: taskDefName, + }; +}); + +export const persistTaskDefinition = assign< + ServiceMethodsMachineContext, + DoneInvokeEvent +>((_context, { data }) => { + return { + currentTaskDefinition: data, + modifiedTaskDef: data, + }; +}); + +export const persistModifiedTaskDef = assign< + ServiceMethodsMachineContext, + HandleChangeTaskConfigEvent +>((context, { name, value }) => { + const updatedTaskDef = { + ...context.modifiedTaskDef, + [name]: value, + }; + return { + modifiedTaskDef: updatedTaskDef, + }; +}); + +export const resetModifiedTaskDef = assign< + ServiceMethodsMachineContext, + HandleChangeTaskConfigEvent +>(({ currentTaskDefinition }) => { + return { + modifiedTaskDef: currentTaskDefinition, + }; +}); diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/state/helper.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/state/helper.ts new file mode 100644 index 0000000000..38dc01e0bb --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/state/helper.ts @@ -0,0 +1,8 @@ +export function parseApiMethod(str: string) { + const match = str?.match(/^\[(\w+)](.+)/); + if (match) { + const [_, methodType, methodName] = match; + return { methodName, methodType }; + } + return { methodName: "", methodType: "" }; // Return empty object if the string doesn't match the format +} diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/state/hook.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/state/hook.ts new file mode 100644 index 0000000000..8b44c3b5c1 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/state/hook.ts @@ -0,0 +1,171 @@ +import { ActorRef } from "xstate"; +import { + ServiceMethodsMachineEvents, + ServiceMethodsMachineEventTypes, + ServiceMethodsMachineStates, +} from "./types"; +import { useSelector } from "@xstate/react"; +import { Method, ServiceDefDto } from "types/RemoteServiceTypes"; +import { useCallback, useMemo, useState } from "react"; +import { parseApiMethod } from "./helper"; +import { useFetch } from "utils/query"; + +export const useServiceMethodsDefinition = ( + serviceMethodsActor: ActorRef, +) => { + const [ + showServiceRegistryPopulatorModal, + setShowServiceRegistryPopulatorModal, + ] = useState(false); + const { send } = serviceMethodsActor; + const { data: schemas } = useFetch("/schema?short=true"); + const services = useSelector( + serviceMethodsActor, + (state) => state.context.services, + ); + + const selectedService = useSelector( + serviceMethodsActor, + (state) => state.context.selectedService, + ); + + const selectedMethod = useSelector( + serviceMethodsActor, + (state) => state.context.selectedMethod, + ); + + const selectedServiceMethods = useMemo(() => { + const methods = selectedService?.methods ?? []; + return methods?.map( + (item: Method) => `[${item.methodType}]` + item.methodName, + ); + }, [selectedService]); + + const currentTaskDefinition = useSelector( + serviceMethodsActor, + (state) => state.context.currentTaskDefinition, + ); + + const isInIdleState = useSelector(serviceMethodsActor, (state) => + state.matches(ServiceMethodsMachineStates.IDLE), + ); + + const selectedHost = useSelector( + serviceMethodsActor, + (state) => state.context.selectedHost, + ); + + const handleSelectService = (serviceName: string) => { + const service = services?.find( + (item: Partial) => item.name === serviceName, + ); + send({ + type: ServiceMethodsMachineEventTypes.SELECT_SERVICE, + data: service, + }); + }; + const handleSelectHost = (host: string) => { + send({ + type: ServiceMethodsMachineEventTypes.SELECT_HOST, + data: host, + }); + }; + + const handleSelectMethod = (method: string) => { + const { methodName, methodType } = parseApiMethod(method); + const result = selectedService?.methods?.find( + (item: Method) => + item.methodName === methodName && item.methodType === methodType, + ); + + send({ + type: ServiceMethodsMachineEventTypes.SELECT_METHOD, + data: result, + }); + }; + + const handleShowServiceRegistryPopulatorModal = (val: boolean) => { + setShowServiceRegistryPopulatorModal(val); + }; + + const handleChangeTaskDefName = useCallback( + (val: string) => { + if (val) { + send({ + type: ServiceMethodsMachineEventTypes.SELECT_TASK, + taskDefName: val, + }); + } + }, + [send], + ); + + const handleChangeTaskConfig = ( + name: string, + value: number | string | null, + ) => { + send({ + type: ServiceMethodsMachineEventTypes.HANDLE_CHANGE_TASK_CONFIG, + name, + value, + }); + }; + + const handleUpdateTaskConfig = () => { + send({ + type: ServiceMethodsMachineEventTypes.UPDATE_TASK_CONFIG, + }); + }; + + const handleResetModifiedTaskConfig = () => { + send({ + type: ServiceMethodsMachineEventTypes.RESET_MODIFIED_TASK_CONFIG, + }); + }; + + const fetchTaskDefinition = useCallback(() => { + send({ + type: ServiceMethodsMachineEventTypes.FETCH_TASK_DEFINITION_EVENT, + }); + }, [send]); + + const handleUpdateTemplate = ({ + updatedUrl, + headers, + }: { + updatedUrl: string; + headers?: Record; + }) => { + send({ + type: ServiceMethodsMachineEventTypes.HANDLE_UPDATE_TEMPLATE, + url: updatedUrl, + headers: headers ?? {}, + }); + }; + + return [ + { + services, + selectedService, + selectedServiceMethods, + selectedMethod, + schemas, + showServiceRegistryPopulatorModal, + currentTaskDefinition, + isInIdleState, + selectedHost, + }, + { + handleSelectService, + handleSelectMethod, + handleSelectHost, + handleShowServiceRegistryPopulatorModal, + handleChangeTaskDefName, + handleChangeTaskConfig, + handleUpdateTaskConfig, + handleResetModifiedTaskConfig, + fetchTaskDefinition, + handleUpdateTemplate, + }, + ] as const; +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/state/machine.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/state/machine.ts new file mode 100644 index 0000000000..215eff0001 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/state/machine.ts @@ -0,0 +1,135 @@ +import { createMachine } from "xstate"; +import * as actions from "./actions"; +import * as services from "./services"; +import { + ServiceMethodsMachineEvents, + ServiceMethodsMachineContext, + ServiceMethodsMachineStates, + ServiceMethodsMachineEventTypes, +} from "./types"; +import { ServiceType } from "types/RemoteServiceTypes"; + +export const serviceMethodsMachine = createMachine< + ServiceMethodsMachineContext, + ServiceMethodsMachineEvents +>( + { + id: "serviceMethodsMachine", + predictableActionArguments: true, + initial: "initial", + context: { + authHeaders: {}, + }, + states: { + initial: { + invoke: { + id: "fetchServices", + src: "fetchServices", + onDone: { + actions: ["persistServices"], + target: ServiceMethodsMachineStates.FETCH_FOR_TASK_DEFINITION, + }, + }, + }, + [ServiceMethodsMachineStates.IDLE]: { + on: { + [ServiceMethodsMachineEventTypes.SELECT_SERVICE]: { + actions: ["persistSelectedService", "maybeChangeTaskType"], + }, + [ServiceMethodsMachineEventTypes.SELECT_HOST]: { + actions: ["persistSelectedHost"], + }, + [ServiceMethodsMachineEventTypes.SELECT_METHOD]: [ + { + cond: ({ selectedService }) => + selectedService?.type === ServiceType.GRPC, + actions: ["persistSelectedMethod"], + target: ServiceMethodsMachineStates.FETCH_FOR_SERVICE_REGISTRY, + }, + { + actions: ["persistSelectedMethod"], + target: ServiceMethodsMachineStates.FETCH_SCHEMA, + }, + ], + [ServiceMethodsMachineEventTypes.SELECT_TASK]: { + actions: ["persistCurrentTaskDefName"], + target: ServiceMethodsMachineStates.FETCH_FOR_TASK_DEFINITION, + }, + [ServiceMethodsMachineEventTypes.HANDLE_CHANGE_TASK_CONFIG]: { + actions: ["persistModifiedTaskDef"], + }, + [ServiceMethodsMachineEventTypes.UPDATE_TASK_CONFIG]: { + target: ServiceMethodsMachineStates.UPDATE_TASK, + }, + [ServiceMethodsMachineEventTypes.RESET_MODIFIED_TASK_CONFIG]: { + actions: ["resetModifiedTaskDef"], + }, + [ServiceMethodsMachineEventTypes.FETCH_TASK_DEFINITION_EVENT]: { + target: ServiceMethodsMachineStates.FETCH_FOR_TASK_DEFINITION, + }, + [ServiceMethodsMachineEventTypes.HANDLE_UPDATE_TEMPLATE]: { + actions: ["templateUpdate"], + target: ServiceMethodsMachineStates.IDLE, + }, + }, + }, + [ServiceMethodsMachineStates.FETCH_FOR_SERVICE_REGISTRY]: { + invoke: { + id: "fetchSchemaForServiceRegistry", + src: "fetchSchemaForServiceRegistry", + onDone: { + actions: ["persistSelectedSchema"], + target: ServiceMethodsMachineStates.IDLE, + }, + onError: { + target: ServiceMethodsMachineStates.IDLE, + }, + }, + }, + [ServiceMethodsMachineStates.FETCH_SCHEMA]: { + invoke: { + id: "fetchSchema", + src: "fetchSchema", + onDone: { + actions: ["persistSelectedSchema"], + target: ServiceMethodsMachineStates.IDLE, + }, + onError: { + target: ServiceMethodsMachineStates.IDLE, + }, + }, + }, + [ServiceMethodsMachineStates.FETCH_FOR_TASK_DEFINITION]: { + invoke: { + id: "fetchTaskDefinition", + src: "fetchTaskDefinition", + onDone: { + actions: ["persistTaskDefinition"], + target: ServiceMethodsMachineStates.IDLE, + }, + onError: { + target: ServiceMethodsMachineStates.IDLE, + }, + }, + }, + [ServiceMethodsMachineStates.UPDATE_TASK]: { + invoke: { + id: "updateTaskDefinitionService", + src: "updateTaskDefinitionService", + onDone: { + actions: ["setSuccessMessage"], + target: ServiceMethodsMachineStates.FETCH_FOR_TASK_DEFINITION, + }, + onError: { + actions: ["setErrorMessage"], + target: ServiceMethodsMachineStates.IDLE, + }, + }, + }, + }, + }, + { + actions: actions as any, + services: services as any, + }, +); diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/state/services.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/state/services.ts new file mode 100644 index 0000000000..9706cb6d90 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/state/services.ts @@ -0,0 +1,127 @@ +import { tryFunc } from "utils/utils"; +import { ServiceMethodsMachineContext } from "./types"; +import { ServiceDefDto } from "types/RemoteServiceTypes"; +import { queryClient } from "queryClient"; + +import { fetchContextNonHook, fetchWithContext } from "plugins/fetch"; +import { ErrorObj } from "types/common"; + +const fetchContext = fetchContextNonHook(); + +export const fetchServices = async ({ + authHeaders: headers, +}: ServiceMethodsMachineContext) => { + const schemaPath = `/registry/service`; + return tryFunc({ + fn: async () => { + return await queryClient.fetchQuery( + [fetchContext.stack, schemaPath], + () => fetchWithContext(schemaPath, fetchContext, { headers }), + ); + }, + customError: { + message: "Fetching services failed!", + }, + showCustomError: false, + }); +}; + +export const fetchSchema = async ({ + authHeaders: headers, + selectedMethod, +}: ServiceMethodsMachineContext) => { + const schemaName = selectedMethod?.inputType; + const schemaPath = `/schema/${schemaName}`; + + if (!schemaName) { + return {}; + } + + return tryFunc({ + fn: async () => { + return await queryClient.fetchQuery( + [fetchContext.stack, schemaPath], + () => fetchWithContext(schemaPath, fetchContext, { headers }), + ); + }, + customError: { + message: "Fetching schema failed!", + }, + showCustomError: false, + }); +}; + +export const fetchSchemaForServiceRegistry = async ({ + authHeaders: headers, + selectedService, +}: ServiceMethodsMachineContext) => { + const schemaName = selectedService?.name; + const schemaPath = `/registry/service/${schemaName}`; + + if (!schemaName) { + return {}; + } + + return tryFunc({ + fn: async () => { + return await queryClient.fetchQuery( + [fetchContext.stack, schemaPath], + () => fetchWithContext(schemaPath, fetchContext, { headers }), + ); + }, + customError: { + message: "Fetching schema failed!", + }, + showCustomError: false, + }); +}; + +export const fetchTaskDefinition = async ({ + authHeaders: headers, + currentTaskDefName, +}: ServiceMethodsMachineContext) => { + if (!currentTaskDefName) { + return; + } + const taskDefinitionPath = `/metadata/taskdefs/${currentTaskDefName}`; + return tryFunc({ + fn: async () => { + return await queryClient.fetchQuery( + [fetchContext.stack, taskDefinitionPath], + () => fetchWithContext(taskDefinitionPath, fetchContext, { headers }), + ); + }, + customError: { + message: "Fetching task definition by name failed!", + }, + showCustomError: false, + }); +}; + +export const updateTaskDefinitionService = async ({ + authHeaders, + modifiedTaskDef, +}: ServiceMethodsMachineContext) => { + const stringDefinition = JSON.stringify(modifiedTaskDef, null, 2); + + return tryFunc({ + fn: async () => { + return await fetchWithContext( + "/metadata/taskdefs", + {}, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + body: stringDefinition, + }, + ); + }, + customError: { + message: "Update task failed!", + }, + showCustomError: false, + }); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/state/types.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/state/types.ts new file mode 100644 index 0000000000..2c4eeffbc3 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/state/types.ts @@ -0,0 +1,93 @@ +import { Method, ServiceDefDto } from "types/RemoteServiceTypes"; +import { AuthHeaders } from "types/common"; +import { TaskDefinitionDto } from "types/TaskDefinition"; + +export interface ServiceMethodsMachineContext { + authHeaders: AuthHeaders; + services?: Partial[]; + selectedService?: Partial; + selectedMethod?: Method; + selectedSchema?: Record; + currentTaskDefName?: string; + currentTaskDefinition?: Partial; + modifiedTaskDef?: Partial; + selectedHost?: string; +} + +export enum ServiceMethodsMachineEventTypes { + SELECT_SERVICE = "SELECT_SERVICE", + SELECT_HOST = "SELECT_HOST", + SELECT_METHOD = "SELECT_METHOD", + SELECT_TASK = "SELECT_TASK", + UPDATE_TASK_CONFIG = "UPDATE_TASK_CONFIG", + HANDLE_CHANGE_TASK_CONFIG = "HANDLE_CHANGE_TASK_CONFIG", + RESET_MODIFIED_TASK_CONFIG = "RESET_MODIFIED_TASK_CONFIG", + FETCH_TASK_DEFINITION_EVENT = "FETCH_TASK_DEFINITION_EVENT", + HANDLE_UPDATE_TEMPLATE = "HANDLE_UPDATE_TEMPLATE", +} + +export enum ServiceMethodsMachineStates { + IDLE = "IDLE", + HANDLE_SELECT_SERVICE = "HANDLE_SELECT_SERVICE", + GO_BACK_TO_IDLE = "GO_BACK_TO_IDLE", + FETCH_SCHEMA = "FETCH_SCHEMA", + FETCH_FOR_SERVICE_REGISTRY = "FETCH_FOR_SERVICE_REGISTRY", + FETCH_FOR_TASK_DEFINITION = "FETCH_FOR_TASK_DEFINITION", + UPDATE_TASK = "UPDATE_TASK", + UPDATE_TEMPLATE = "UPDATE_TEMPLATE", +} + +export type SelectServiceNameEvent = { + type: ServiceMethodsMachineEventTypes.SELECT_SERVICE; + data: Partial; +}; + +export type SelectMethodEvent = { + type: ServiceMethodsMachineEventTypes.SELECT_METHOD; + data: Method; +}; + +export type SelectTaskEvent = { + type: ServiceMethodsMachineEventTypes.SELECT_TASK; + taskDefName: string; +}; + +export type UpdateTaskConfigEvent = { + type: ServiceMethodsMachineEventTypes.UPDATE_TASK_CONFIG; +}; + +export type HandleChangeTaskConfigEvent = { + type: ServiceMethodsMachineEventTypes.HANDLE_CHANGE_TASK_CONFIG; + name: string; + value: number | string | null; +}; + +export type HandleResetModifiedTaskConfigEvent = { + type: ServiceMethodsMachineEventTypes.RESET_MODIFIED_TASK_CONFIG; +}; + +export type FetchForTaskDefinitionEvent = { + type: ServiceMethodsMachineEventTypes.FETCH_TASK_DEFINITION_EVENT; +}; + +export type HandleUpdateTemplateEvent = { + type: ServiceMethodsMachineEventTypes.HANDLE_UPDATE_TEMPLATE; + url: string; + headers?: Record; +}; + +export type SelectHostEvent = { + type: ServiceMethodsMachineEventTypes.SELECT_HOST; + data: string; +}; + +export type ServiceMethodsMachineEvents = + | SelectServiceNameEvent + | SelectMethodEvent + | SelectTaskEvent + | UpdateTaskConfigEvent + | HandleChangeTaskConfigEvent + | HandleResetModifiedTaskConfigEvent + | FetchForTaskDefinitionEvent + | HandleUpdateTemplateEvent + | SelectHostEvent; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/types.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/types.ts new file mode 100644 index 0000000000..dc15c78de2 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HTTPTaskForm/types.ts @@ -0,0 +1,6 @@ +import { TaskFormProps } from "../types"; +import { HttpTaskDef } from "types"; + +export interface HttpTaskFormProps extends TaskFormProps { + task: HttpTaskDef; +} diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HedgingConfigForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HedgingConfigForm.tsx new file mode 100644 index 0000000000..f5deeb1c5c --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/HedgingConfigForm.tsx @@ -0,0 +1,64 @@ +import { Box } from "@mui/material"; +import MuiTypography from "components/MuiTypography"; +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import ConductorTooltip from "components/conductorTooltip/ConductorTooltip"; + +interface HedgingConfigFormProp { + hedgingConfig?: { maxAttempts?: number }; + onChange: (value: any) => void; +} + +function HedgingConfigForm({ + hedgingConfig = {}, + onChange, +}: HedgingConfigFormProp) { + return ( + + + Hedging config + +
  • + When enabled, the system will make parallel requests and take + the response from the first successful call. +
  • +
  • + Hedging allows for normalizing tail latencies in remote + services. +
  • +
  • + Please note: Hedging makes parallels requests, so make sure to + only use for services that are idempotent. +
  • + + } + placement="top" + children={ + info + } + /> +
    + onChange({ ...hedgingConfig, maxAttempts: val })} + coerceTo="integer" + /> +
    + ); +} + +export default HedgingConfigForm; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/INLINETaskForm/INLINETaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/INLINETaskForm/INLINETaskForm.tsx new file mode 100644 index 0000000000..b7785d8e43 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/INLINETaskForm/INLINETaskForm.tsx @@ -0,0 +1,133 @@ +import { Box, FormControlLabel, Grid } from "@mui/material"; +import RadioButtonGroup from "components/RadioButtonGroup"; +import { ConductorFlatMapFormBase } from "components/v1/FlatMapForm/ConductorFlatMapForm"; +import _path from "lodash/fp/path"; +import { colors } from "theme/tokens/variables"; +import { InlineTaskDef } from "types"; +import { featureFlags, FEATURES } from "utils"; +import { updateField } from "utils/fieldHelpers"; +import { Optional } from "../OptionalFieldForm"; +import TaskFormSection from "../TaskFormSection"; +import { TaskFormProps } from "../types"; +import InlineCodeBlock from "./InlineCodeBlock"; + +const hideJavascriptOption = featureFlags.isEnabled( + FEATURES.HIDE_JAVASCRIPT_OPTION, +); + +export const INLINETaskForm = ({ task, onChange }: TaskFormProps) => { + const isJavascriptVisible = + task?.inputParameters?.evaluatorType === "javascript"; + + let options = []; + if (hideJavascriptOption) { + options = [ + { + value: "graaljs", + label: "ECMASCRIPT", + }, + ]; + } else { + options = [ + { + value: "graaljs", + label: "ECMASCRIPT", + }, + ]; + if (isJavascriptVisible) { + const javascriptOption = { + value: "javascript", + label: "Javascript(deprecated)", + disabled: true, + }; + options = [...options, javascriptOption]; + } + } + + return ( + + + + + + onChange(updateField("inputParameters", data, task)) + } + value={{ ...(task?.inputParameters || {}) }} + /> + + + + + + {isJavascriptVisible && ( + + { + onChange( + updateField( + "inputParameters.evaluatorType", + value, + task, + ), + ); + }} + /> + } + label="Script:" + sx={{ + marginLeft: 0, + "& .MuiFormControlLabel-label": { + fontWeight: 600, + color: colors.gray07, + }, + }} + /> + + )} + + + } + onChange={onChange} + /> + + + + + + + + + + ); +}; + +export default INLINETaskForm; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/INLINETaskForm/InlineCodeBlock.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/INLINETaskForm/InlineCodeBlock.tsx new file mode 100644 index 0000000000..9e1be3c560 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/INLINETaskForm/InlineCodeBlock.tsx @@ -0,0 +1,227 @@ +import { EditorProps, Monaco } from "@monaco-editor/react"; +import { BoxProps } from "@mui/material"; +import { Theme } from "@mui/material/styles"; +import { SxProps } from "@mui/system"; +import { ConductorCodeBlockInput } from "components/v1/ConductorCodeBlockInput"; +import _keys from "lodash/keys"; +import { + invalidDollarVariables, + undeclaredInputParameters, +} from "pages/definition/helpers"; +import { + CSSProperties, + FunctionComponent, + MutableRefObject, + ReactNode, + useCallback, + useContext, + useEffect, + useRef, +} from "react"; +import { ColorModeContext } from "theme/material/ColorModeContext"; +import { InlineTaskDef } from "types"; +import { + OnlyTheWordInfoProp, + editorAddCommandAltEnter, + editorDecorations, +} from "../../helpers"; +import { smallEditorDefaultOptions } from "../editorConfig"; +import { logger } from "utils/logger"; + +type InlineCodeBlockProps = { + label?: ReactNode; + language?: string; + onChange?: (taskChanges: Partial) => void; + containerProps?: BoxProps; + error?: boolean; + height?: number | "auto"; + minHeight?: number; + autoformat?: boolean; + labelStyle?: SxProps; + languageLabel?: string; + containerStyles?: CSSProperties; + autoSizeBox?: boolean; + task: Partial; +} & Partial>; + +const MIN_HEIGHT = 120; + +const additionalEditorOptions = { + lineNumbers: "on" as const, + lineDecorationsWidth: 10, +}; + +const warnUndeclaredVariables = ( + editor: Monaco, + monaco: any, + task: Partial, + currentDecorations: MutableRefObject, +) => { + const model = editor.getModel(); + const taskExpression = task?.inputParameters?.expression; + if (model && taskExpression && editor) { + const addedInputParameters = undeclaredInputParameters( + model.getValue(), + task?.inputParameters, + ); + + const invalidDollarVars = invalidDollarVariables(model.getValue()); + + const decorations = editorDecorations( + model, + [...addedInputParameters, ...invalidDollarVars], + monaco, + ); + + return editor.deltaDecorations( + currentDecorations.current ? currentDecorations.current : [], + decorations.flat(), + ); + } +}; + +const InlineCodeBlock: FunctionComponent = ({ + label = "Code", + language = "json", + onChange = () => null, + minHeight, + autoSizeBox = false, + task, + ...restOfProps +}) => { + const taskRef = useRef | null>(null); + taskRef.current = task; + const { mode } = useContext(ColorModeContext); + const disposeRef = useRef(null) as any; + const currentDecorations = useRef([]) as any; + + useEffect(() => { + return () => { + if (disposeRef.current) { + disposeRef.current(); + } + }; + }, []); + + const handleEditorDidMount = useCallback( + (editor: Monaco, monaco: any) => { + const model = editor.getModel(); + + const callBackFunction = (onlyTheWordInfo: OnlyTheWordInfoProp) => { + onChange({ + ...taskRef.current, + inputParameters: { + ...taskRef.current!.inputParameters, + [onlyTheWordInfo.word]: "", // Add the original word + expression: model.getValue(), + }, + } as Partial); + // cleanup + currentDecorations.current = warnUndeclaredVariables( + editor, + monaco, + taskRef.current!, + currentDecorations, + ); + }; + // editor.AddCommand function + editorAddCommandAltEnter(editor, monaco, taskRef, callBackFunction); + + editor.onDidChangeModelContent((_event: any) => { + // Warn on change + currentDecorations.current = warnUndeclaredVariables( + editor, + monaco, + taskRef.current!, + currentDecorations, + ); + }); + + currentDecorations.current = warnUndeclaredVariables( + editor, + monaco, + taskRef.current!, + currentDecorations, + ); + }, + [onChange], + ); + + const onEditorChange = useCallback( + (editorValue: string) => { + onChange({ + ...taskRef.current, + inputParameters: { + ...taskRef.current?.inputParameters, + expression: editorValue, + }, + } as Partial); + }, + [onChange], + ); + + const minimumHeight = minHeight || MIN_HEIGHT; + + return ( + { + handleEditorDidMount(editor, monaco); + }} + beforeMount={(monaco: Monaco) => { + if (disposeRef.current) { + try { + disposeRef.current(); + } catch (error) { + logger.error("Error disposing from Ref on beforeMount", error); + } + disposeRef.current = null; + } + const disposable = monaco.languages.registerCompletionItemProvider( + "javascript", + { + provideCompletionItems: () => { + const inputVariables = _keys(taskRef?.current?.inputParameters); + let variableSuggestions: string[] = []; + if (inputVariables) { + variableSuggestions = inputVariables + .filter( + (item) => item !== "expression" && item !== "evaluatorType", + ) + .map((item) => `$.${item}`); + } + // Provide suggestions for JSON properties that start with the current text + const propertySuggestions = variableSuggestions.map( + (property) => ({ + label: property, + kind: monaco.languages.CompletionItemKind.Value, + insertText: `${property}`, + }), + ); + // Merge custom suggestions with JSON property suggestions + const suggestions = [...propertySuggestions]; + return { suggestions }; + }, + }, + ); + + disposeRef.current = () => disposable.dispose(); + }} + width="100%" + height={autoSizeBox ? "auto" : minimumHeight} + minHeight={minimumHeight} + defaultLanguage={language} + options={{ + ...smallEditorDefaultOptions, + ...(autoSizeBox && { scrollBeyondLastLine: false }), + ...additionalEditorOptions, + }} + value={taskRef?.current?.inputParameters?.expression || ""} + {...restOfProps} + /> + ); +}; + +export default InlineCodeBlock; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/INLINETaskForm/index.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/INLINETaskForm/index.ts new file mode 100644 index 0000000000..5126400387 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/INLINETaskForm/index.ts @@ -0,0 +1 @@ +export * from "./INLINETaskForm"; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/JDBCTaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/JDBCTaskForm.tsx new file mode 100644 index 0000000000..5ae21454eb --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/JDBCTaskForm.tsx @@ -0,0 +1,175 @@ +import { Box, Grid } from "@mui/material"; +import RadioButtonGroup from "components/RadioButtonGroup"; +import { ConductorArrayField } from "components/v1/ConductorArrayField"; +import { ConductorCodeBlockInput } from "components/v1/ConductorCodeBlockInput"; +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import { assoc as _assoc, path as _path } from "lodash/fp"; +import { ChangeEvent } from "react"; +import { UseQueryResult } from "react-query"; +import { IntegrationCategory, IntegrationDef, JDBCType, TaskType } from "types"; +import { updateField } from "utils/fieldHelpers"; +import { useIntegrationProviders } from "utils/useIntegrationProviders"; +import { ConductorCacheOutput } from "./ConductorCacheOutputForm"; +import { Optional } from "./OptionalFieldForm"; + +import TaskFormSection from "./TaskFormSection"; +import { TaskFormProps } from "./types"; + +const connectionIdPath = "inputParameters.connectionId"; +const integrationNamePath = "inputParameters.integrationName"; +const expectedUpdateCountPath = "inputParameters.expectedUpdateCount"; +const jdbcTypePath = "inputParameters.type"; +const queryParametersPath = "inputParameters.parameters"; +const statementPath = "inputParameters.statement"; + +export const JDBCTaskForm = ({ task, onChange }: TaskFormProps) => { + const { data: integrationDBNames }: UseQueryResult = + useIntegrationProviders({ + category: IntegrationCategory.RELATIONAL_DB, + activeOnly: false, + }); + + const queryParameters = _path(queryParametersPath, task); + + const changeQueryParameters = (value: string[]) => { + onChange(updateField(queryParametersPath, value, task)); + }; + + return ( + + + + {!!task.inputParameters?.connectionId && ( + + + onChange(updateField(connectionIdPath, changes, task)) + } + value={_path(connectionIdPath, task)} + label="Connection id (Deprecated)" + disabled + /> + + )} + + item.name) || []} + onChange={(changes) => + onChange(updateField(integrationNamePath, changes, task)) + } + value={_path(integrationNamePath, task)} + label="Integration name" + /> + + + + + + + + ) => + onChange(_assoc(jdbcTypePath, val.target.value, task)) + } + items={[ + { + value: JDBCType.SELECT, + label: "SELECT", + }, + { + value: JDBCType.UPDATE, + label: "INSERT/UPDATE/DELETE", + }, + ]} + name="jdbcType" + /> + + {_path(jdbcTypePath, task) === JDBCType.UPDATE && ( + + + onChange( + updateField(expectedUpdateCountPath, changes, task), + ) + } + value={_path(expectedUpdateCountPath, task)} + label="Expected update count" + inputProps={{ + tooltip: { + title: "Expected update count", + content: + "If you have chosen ‘UPDATE’ as the statement type, provide the number of rows you need to update in the database.", + }, + }} + /> + + )} + + + + + + onChange(updateField(statementPath, changes, task)) + } + /> + + + + + + + + + + + + + + + + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/JOINTaskForm/JOINTaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/JOINTaskForm/JOINTaskForm.tsx new file mode 100644 index 0000000000..d07189c0b5 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/JOINTaskForm/JOINTaskForm.tsx @@ -0,0 +1,309 @@ +import { Box, Grid, Stack, Typography } from "@mui/material"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import { useSelector } from "@xstate/react"; +import Button from "components/MuiButton"; +import MuiCheckbox from "components/MuiCheckbox"; +import MuiTypography from "components/MuiTypography"; +import ConfirmChoiceDialog from "components/ConfirmChoiceDialog"; +import { + crumbsToTaskSteps, + forkLastTaskReferences, + tasksAsNodes, +} from "components/flow/nodes/mapper"; +import { ConductorFlatMapFormBase } from "components/v1/FlatMapForm/ConductorFlatMapForm"; +import _difference from "lodash/difference"; +import _first from "lodash/first"; +import { path as _path } from "lodash/fp"; +import _initial from "lodash/initial"; +import _isEqual from "lodash/isEqual"; +import _last from "lodash/last"; +import _nth from "lodash/nth"; +import { WorkflowEditContext } from "pages/definition/state"; +import { useCallback, useContext, useEffect, useMemo, useState } from "react"; +import { JoinTaskDef, TaskDef, TaskType } from "types/index"; +import { updateField } from "utils/fieldHelpers"; +import { Optional } from "../OptionalFieldForm"; +import TaskFormSection from "../TaskFormSection"; +import { TaskFormProps } from "../types"; +import { JoinCodeBlock } from "./JoinCodeBlock"; + +const DEFAULT_EXPRESSION = + '(function(){\n let results = {};\n let pendingJoinsFound = false;\n if($.joinOn){\n $.joinOn.forEach((element)=>{\n if($[element] && $[element].status !== \'COMPLETED\'){\n results[element] = $[element].status;\n pendingJoinsFound = true;\n }\n });\n if(pendingJoinsFound){\n return {\n "status":"IN_PROGRESS",\n "reasonForIncompletion":"Pending",\n "outputData":{\n "scriptResults": results\n }\n };\n }\n // To complete the Join - return true OR an object with status = \'COMPLETED\' like above.\n return true;\n }\n})();'; + +const EXPRESSION_PATH = "expression"; +const INPUT_PARAMETERS_PATH = "inputParameters"; + +export const JOINTaskForm = ({ task, onChange }: TaskFormProps) => { + const [possibleTaskReferences, setPossibleTaskReferences] = useState< + string[] + >([]); + + const [showConfirmOverrideDialog, setShowConfirmOverrideDialog] = + useState(false); + + const { workflowDefinitionActor } = useContext(WorkflowEditContext); + const selectedTaskCrumbs = useSelector( + workflowDefinitionActor!, + (state) => state.context.selectedTaskCrumbs, + ); + const editorTasks = useSelector( + workflowDefinitionActor!, + (state) => state.context.workflowChanges.tasks, + ); + + const tasksInCrumbBranch = useMemo(() => { + return _initial(crumbsToTaskSteps(selectedTaskCrumbs, editorTasks)); + }, [editorTasks, selectedTaskCrumbs]); + + const forkLastTaskReferencesWrapper = async (forkTask: TaskDef[]) => { + if (forkTask?.length > 0 && _last(forkTask)?.type === TaskType.TERMINATE) { + return []; + } + if (forkTask?.length === 1 && _first(forkTask)?.type === TaskType.SWITCH) { + return [_first(forkTask)?.taskReferenceName]; + } + return forkLastTaskReferences(forkTask, tasksAsNodes); + }; + + useEffect(() => { + const forkTasksInBranch = tasksInCrumbBranch.reduce( + (acc: any, ct: any, idx: number) => + ct.type === TaskType.FORK_JOIN + ? acc.concat( + Promise.all( + ct.forkTasks.map((t: TaskDef[]) => + forkLastTaskReferencesWrapper(t), + ), + ).then((trList) => { + return _difference( + trList.flat(), + (_nth(tasksInCrumbBranch, idx + 1) as any)?.joinOn || [], + ); + }), + ) + : acc, + [], + ); + async function setPossibleTaskReferencesAsync( + upperForkTask: Promise[], + ) { + const taskReferences = await Promise.all(upperForkTask); + setPossibleTaskReferences(taskReferences.flat()); + } + setPossibleTaskReferencesAsync(forkTasksInBranch); + }, [tasksInCrumbBranch]); + + const onChangeHandler = useCallback( + (a: any) => { + if (!a || !a.target) return; + const { name, checked } = a.target; + const currentSelections = task.joinOn; + const validCurrentSelections = possibleTaskReferences.filter((tr) => + currentSelections?.includes(tr), + ); + onChange({ + ...task, + joinOn: checked + ? validCurrentSelections.concat(name) + : validCurrentSelections.filter((n) => n !== name), + }); + }, + [onChange, possibleTaskReferences, task], + ); + + const handleApplySampleScript = useCallback(() => { + onChange(updateField(EXPRESSION_PATH, DEFAULT_EXPRESSION, task)); + }, [task, onChange]); + + const checkEveryJoin = useCallback(() => { + if (possibleTaskReferences.length > 0) { + onChange(updateField("joinOn", possibleTaskReferences, task)); + } + }, [task, onChange, possibleTaskReferences]); + + const unSelectAll = () => { + onChange(updateField("joinOn", [], task)); + }; + + const hasScriptExpression = useMemo((): boolean => { + return _path(EXPRESSION_PATH, task) != null; + }, [task]); + + const toggleScriptExpression = useCallback(() => { + if (hasScriptExpression) { + onChange({ ...task, expression: undefined, evaluatorType: undefined }); + } else { + onChange({ ...task, expression: "", evaluatorType: "js" }); + } + }, [task, onChange, hasScriptExpression]); + + const isEveryJoinSelected = useMemo(() => { + const selectedJoins = task?.joinOn || []; + return _isEqual(selectedJoins.sort(), possibleTaskReferences.sort()); + }, [task, possibleTaskReferences]); + + return ( + + + + Input joins + + + + } + accordionAdditionalProps={{ defaultExpanded: true }} + > + 0 ? 2 : 1} + sx={{ width: "100%" }} + id="input-joins-section" + > + {possibleTaskReferences?.map((forkTaskReferenceName) => ( + + + } + label={forkTaskReferenceName} + /> + + ))} + + + + + + + + onChange(updateField(INPUT_PARAMETERS_PATH, value, task)) + } + autoFocusField={false} + /> + + + + + + + + + } + label={"Use scripting to determine join"} + /> + + + + When checked, you must provide a script to control how the join + task completes. The script will have access to a variable called{" "} + $.joinOn which is an array of the task references + mapped to this join, and the output data of each joined task, such + as $['task-reference-name'] + + + + + setShowConfirmOverrideDialog(true)} + control={ + + } + label={"Apply sample script template"} + /> + + {hasScriptExpression ? ( + } + onChange={onChange} + /> + ) : null} + + + {showConfirmOverrideDialog && ( + { + if (confirmed) { + handleApplySampleScript(); + } + setShowConfirmOverrideDialog(false); + }} + message={ + "Applying the sample script will overwrite any existing script. Are you sure you want to proceed?" + } + /> + )} + + + + + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/JOINTaskForm/JoinCodeBlock.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/JOINTaskForm/JoinCodeBlock.tsx new file mode 100644 index 0000000000..8ec21d2354 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/JOINTaskForm/JoinCodeBlock.tsx @@ -0,0 +1,248 @@ +import { EditorProps, Monaco } from "@monaco-editor/react"; +import { BoxProps } from "@mui/material"; +import { Theme } from "@mui/material/styles"; +import { SxProps } from "@mui/system"; +import _keys from "lodash/keys"; + +import { ConductorCodeBlockInput } from "components/v1/ConductorCodeBlockInput"; +import { + invalidDollarVariables, + undeclaredInputParameters, +} from "pages/definition/helpers"; +import { + CSSProperties, + FunctionComponent, + MutableRefObject, + ReactNode, + useCallback, + useContext, + useEffect, + useRef, +} from "react"; +import { ColorModeContext } from "theme/material/ColorModeContext"; +import { JoinTaskDef } from "types"; +import { editorDecorations } from "../../helpers"; +import { smallEditorDefaultOptions } from "../editorConfig"; + +type JoinCodeBlockProps = { + label?: ReactNode; + language?: string; + onChange?: (taskChanges: Partial) => void; + containerProps?: BoxProps; + error?: boolean; + height?: number | "auto"; + minHeight?: number; + autoformat?: boolean; + labelStyle?: SxProps; + languageLabel?: string; + containerStyles?: CSSProperties; + autoSizeBox?: boolean; + task: Partial; +} & Partial>; + +const MIN_HEIGHT = 120; + +const additionalEditorOptions = { + lineNumbers: "on" as const, + lineDecorationsWidth: 10, +}; + +const warnUndeclaredVariables = ( + editor: Monaco, + monaco: any, + task: Partial, + currentDecorations: MutableRefObject, +) => { + const model = editor.getModel(); + const taskExpression = task?.expression; + if (model && taskExpression && editor) { + const addedInputParameters = undeclaredInputParameters( + model.getValue(), + task?.inputParameters, + ); + let filteredInputParameters = [...addedInputParameters]; + + if (addedInputParameters.includes("joinOn")) { + filteredInputParameters = addedInputParameters.filter( + (item) => item !== "joinOn", + ); + } + + const invalidDollarVars = invalidDollarVariables(model.getValue()); + + const decorations = editorDecorations( + model, + [...filteredInputParameters, ...invalidDollarVars], + monaco, + ); + + return editor.deltaDecorations( + currentDecorations.current ? currentDecorations.current : [], + decorations.flat(), + ); + } +}; +const VARIABLE_DEFINER = "$."; +const EXEMPTED_KEYS = ["$.joinOn"]; + +export const JoinCodeBlock: FunctionComponent = ({ + language = "json", + onChange = () => null, + minHeight, + autoSizeBox = false, + task, + ...restOfProps +}) => { + const taskRef = useRef | null>(null); + taskRef.current = task; + const { mode } = useContext(ColorModeContext); + const disposeRef = useRef void)>(null); + const currentDecorations = useRef([]) as any; + + useEffect(() => { + return () => { + if (disposeRef.current) { + disposeRef.current(); + } + }; + }, []); + + const handleEditorDidMount = useCallback( + (editor: Monaco, monaco: any) => { + editor.addCommand(monaco.KeyMod.Alt | monaco.KeyCode.Enter, () => { + const position = editor.getPosition(); // Get the current cursor position + const model = editor.getModel(); + + if (model) { + const onlyTheWordInfo = model.getWordAtPosition(position); // This only selects the word + + const startColumn = onlyTheWordInfo?.startColumn; + if (startColumn > VARIABLE_DEFINER.length) { + // Avoid blowing up because of wrong position. + const newStart = Math.max(startColumn - VARIABLE_DEFINER.length, 1); // We select a new start + let word = null; + // Create a new range from th new start including $. + const wordRange = new monaco.Range( + position.lineNumber, + newStart, + position.lineNumber, + onlyTheWordInfo.endColumn, + ); + word = model.getValueInRange(wordRange); + + if ( + word && + word?.includes(VARIABLE_DEFINER) && + !EXEMPTED_KEYS.includes(word) + ) { + const maybeNewVariable = word.word; + const currentVariables = _keys( + taskRef.current?.inputParameters || {}, + ); + + if (!currentVariables.includes(maybeNewVariable)) { + onChange({ + ...taskRef.current, + inputParameters: { + ...taskRef.current!.inputParameters, + [onlyTheWordInfo.word]: "", // Add the original word + }, + } as Partial); + // cleanup + currentDecorations.current = warnUndeclaredVariables( + editor, + monaco, + taskRef.current!, + currentDecorations, + ); + } + } + } + } + }); + editor.onDidChangeModelContent((_event: any) => { + // Warn on change + currentDecorations.current = warnUndeclaredVariables( + editor, + monaco, + taskRef.current!, + currentDecorations, + ); + }); + + // Warn on mount + currentDecorations.current = warnUndeclaredVariables( + editor, + monaco, + taskRef.current!, + currentDecorations, + ); + }, + [onChange], + ); + + const onEditorChange = useCallback( + (editorValue: string) => { + onChange({ + ...taskRef.current, + expression: editorValue, + } as Partial); + }, + [onChange], + ); + + const minimumHeight = minHeight || MIN_HEIGHT; + + return ( + { + handleEditorDidMount(editor, monaco); + }} + beforeMount={(monaco: Monaco) => { + if (disposeRef.current) { + disposeRef.current(); + disposeRef.current = null; + } + const disposable = monaco.languages.registerCompletionItemProvider( + "javascript", + { + provideCompletionItems: () => { + const inputVariables = _keys(taskRef?.current?.inputParameters); + let variableSuggestions: string[] = []; + if (inputVariables) { + variableSuggestions = inputVariables.map((item) => `$.${item}`); + } + variableSuggestions.push("$.joinOn"); + // Provide suggestions for JSON properties that start with the current text + const propertySuggestions = variableSuggestions.map( + (property) => ({ + label: property, + kind: monaco.languages.CompletionItemKind.Value, + insertText: `${property}`, + }), + ); + // Merge custom suggestions with JSON property suggestions + const suggestions = [...propertySuggestions]; + return { suggestions }; + }, + }, + ); + // IMPORTANT: keep `dispose()` bound to its disposable context. + // Destructuring `dispose` can lose `this` and throw "Unbound disposable context". + disposeRef.current = () => disposable.dispose(); + }} + width="100%" + height={autoSizeBox ? "auto" : minimumHeight} + defaultLanguage={language} + options={{ + ...smallEditorDefaultOptions, + ...(autoSizeBox && { scrollBeyondLastLine: false }), + ...additionalEditorOptions, + }} + value={taskRef?.current?.expression || ""} + {...restOfProps} + /> + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/JOINTaskForm/index.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/JOINTaskForm/index.ts new file mode 100644 index 0000000000..7ad8cba2c0 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/JOINTaskForm/index.ts @@ -0,0 +1 @@ +export * from "./JOINTaskForm"; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/JSONField.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/JSONField.tsx new file mode 100644 index 0000000000..301e6306d7 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/JSONField.tsx @@ -0,0 +1,55 @@ +import { cloneElement, FunctionComponent } from "react"; +import { castToBooleanIfIsBooleanString } from "utils/utils"; +import { clone, path as _path } from "lodash/fp"; +import { updateField } from "utils/fieldHelpers"; + +export interface JSONFieldProps { + path: string; + onChange?: (value: any) => void; + taskJson: any; + checked?: boolean; + children: any; + enableCastToBoolean?: boolean; +} +const JSONField: FunctionComponent = ({ + path, + onChange, + taskJson, + checked, + children, + enableCastToBoolean = true, +}: JSONFieldProps) => { + return cloneElement(children, { + value: clone(_path(path, taskJson)), + checked: checked, + // Needed for special fields like the SinkSelector in EventTaskForm. + /* taskJson: taskJson, */ + onChange: (maybeEventOrValue: any, maybeValue: any) => { + // Guarding to automatically detect different types of event handlers + // working with different onChange signatures. + let newValue; + + // If the onChange signature is (event, value) + if (maybeEventOrValue?.target && maybeValue !== undefined) { + newValue = maybeValue; + // ...if it's just (event) + } else if (maybeEventOrValue?.nativeEvent && maybeValue === undefined) { + newValue = maybeEventOrValue.target.value; + // ...if it's just (value) + } else if (maybeEventOrValue) { + newValue = maybeEventOrValue; + } + + if (enableCastToBoolean) { + newValue = castToBooleanIfIsBooleanString(newValue); + } + + // if the outer onChange is defined, validate the value + if (onChange) { + onChange(updateField(path, newValue, taskJson)); + } + }, + }); +}; + +export default JSONField; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/JSONJQTransformForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/JSONJQTransformForm.tsx new file mode 100644 index 0000000000..1cb659b7c0 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/JSONJQTransformForm.tsx @@ -0,0 +1,66 @@ +import { Box, Grid } from "@mui/material"; +import { ConductorCodeBlockInput } from "components/v1/ConductorCodeBlockInput"; +import { ConductorFlatMapFormBase } from "components/v1/FlatMapForm/ConductorFlatMapForm"; +import _path from "lodash/fp/path"; +import { updateField } from "utils/fieldHelpers"; +import { configureJQLanguage } from "utils/monacoUtils/CodeEditorUtils"; +import JSONField from "./JSONField"; +import { Optional } from "./OptionalFieldForm"; + +import TaskFormSection from "./TaskFormSection"; +import { TaskFormProps } from "./types"; + +const queryExpressionPath = "inputParameters.queryExpression"; + +export const JSONJQTransformForm = ({ task, onChange }: TaskFormProps) => { + return ( + + + + + + + + + + + + + + + onChange(updateField(queryExpressionPath, value, task)) + } + /> + + + + + + + + + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/KafkaTaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/KafkaTaskForm.tsx new file mode 100644 index 0000000000..88e3dd3c6a --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/KafkaTaskForm.tsx @@ -0,0 +1,127 @@ +import { Grid, Box } from "@mui/material"; +import TaskFormSection from "./TaskFormSection"; +import { TaskFormProps } from "./types"; + +import { MaybeVariable } from "./MaybeVariable"; +import { useGetSetHandler } from "./useGetSetHandler"; +import { TaskType } from "types"; +import { Optional } from "./OptionalFieldForm"; +import { ConductorFlatMapForm } from "components/v1/FlatMapForm/ConductorFlatMapForm"; +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import { ConductorKeyValueInput } from "components/v1/FlatMapForm/ConductorKeyValueInput"; + +const KAFKA_REQUEST = "inputParameters.kafka_request"; +const TOPIC_PATH = `${KAFKA_REQUEST}.topic`; +const TOPIC_VALUE = `${KAFKA_REQUEST}.value`; +const BOOTSTRAP_SERVER_PATH = `${KAFKA_REQUEST}.bootStrapServers`; +const HEADERS_PATH = `${KAFKA_REQUEST}.headers`; +const KEY_PATH = `${KAFKA_REQUEST}.key`; +const KEY_SERIALIZER_PATH = `${KAFKA_REQUEST}.keySerializer`; + +export const KafkaTaskForm = (props: TaskFormProps) => { + const { task, onChange } = props; + const [kafkaRequest, handleKafkaRequest] = useGetSetHandler( + props, + KAFKA_REQUEST, + ); + const [topic, handleTopicChange] = useGetSetHandler(props, TOPIC_PATH); + const [topicValue, handleTopicValue] = useGetSetHandler(props, TOPIC_VALUE); + const [bootstrapServer, handlerBootstrapServerPath] = useGetSetHandler( + props, + BOOTSTRAP_SERVER_PATH, + ); + const [headers, handlerHeadersPath] = useGetSetHandler(props, HEADERS_PATH); + const [key, handlerKey] = useGetSetHandler(props, KEY_PATH); + const [keySerializer, handlerKeySerializer] = useGetSetHandler( + props, + KEY_SERIALIZER_PATH, + ); + + return ( + + + + + {}} + value={topicValue} + onChangeKey={(newKey) => { + handleTopicChange(newKey); + }} + onChangeValue={(newValue: any) => { + handleTopicValue(newValue); + }} + hideButtons={true} + keyColumnLabel={"Topic:"} + valueColumnLabel={"Value:"} + enableAutocomplete={true} + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMChainTaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMChainTaskForm.tsx new file mode 100644 index 0000000000..cf0ef2d676 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMChainTaskForm.tsx @@ -0,0 +1,124 @@ +import { Grid, Box } from "@mui/material"; +import JSONField from "./JSONField"; +import Input from "components/Input"; +import TaskFormSection from "./TaskFormSection"; +import { TaskFormProps } from "./types"; +import Dropdown from "components/Dropdown"; +import MuiButton from "components/MuiButton"; +import { Link } from "react-router"; + +const LLMChainTaskForm = ({ task, onChange }: TaskFormProps) => ( + + + + + + + + + + + Prompt description: + + + + This prompt generates movie synopsis, pulled from a text, pdf, URL + or database.View more + + Test + + + + + + + + + + + + + + + + + + + + #1 Prompt variable description: + + + This prompt generates movie synopsis, pulled from a text, pdf, URL + or database.View more + + + + + + + + + + + #2 Prompt variable description: + + + This prompt generates movie synopsis, pulled from a text, pdf, URL + or database.View more + + + + + + +); +export default LLMChainTaskForm; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMChatCompleteTaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMChatCompleteTaskForm.tsx new file mode 100644 index 0000000000..e84a7f79c7 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMChatCompleteTaskForm.tsx @@ -0,0 +1,106 @@ +import { Box } from "@mui/material"; +import { UiIntegrationsFieldType } from "types/FormFieldTypes"; +import { fieldsToFieldsFieldsComponents } from "utils/fieldHelpers"; +import { ConductorCacheOutput } from "./ConductorCacheOutputForm"; +import { LLMFormFields } from "./LLMFormFields/LLMFormFields"; +import LLMFormFieldsWrapper from "./LLMFormFields/LLMFormFieldsWrapper"; +import { Optional } from "./OptionalFieldForm"; +import TaskFormSection from "./TaskFormSection"; +import { TaskFormProps } from "./types"; + +const modelFields = [ + UiIntegrationsFieldType.LLM_PROVIDER, + UiIntegrationsFieldType.MODEL, +]; + +const promptFields = [UiIntegrationsFieldType.INSTRUCTIONS]; + +const messageFields = [UiIntegrationsFieldType.MESSAGES]; + +const fineTuningFields = [ + UiIntegrationsFieldType.TEMPERATURE, + UiIntegrationsFieldType.TOP_P, + UiIntegrationsFieldType.MAX_TOKENS, + UiIntegrationsFieldType.STOP_WORDS, +]; + +const outputFields = [UiIntegrationsFieldType.JSON_OUTPUT]; + +const modelFieldComponents = fieldsToFieldsFieldsComponents(modelFields); +const promptFieldComponents = fieldsToFieldsFieldsComponents(promptFields); +const messageFieldComponents = fieldsToFieldsFieldsComponents(messageFields); +const fineTuningFieldComponents = + fieldsToFieldsFieldsComponents(fineTuningFields); +const outputFieldComponents = fieldsToFieldsFieldsComponents(outputFields); + +const allFieldComponents = [ + ...modelFieldComponents, + ...promptFieldComponents, + ...messageFieldComponents, + ...fineTuningFieldComponents, + ...outputFieldComponents, +]; + +export const LLMChatCompleteTaskForm = ({ task, onChange }: TaskFormProps) => { + return ( + + {(actor) => ( + + + + + + + + + + + + + + + + + + + + + + + + )} + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields/ConductorArrayMapForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields/ConductorArrayMapForm.tsx new file mode 100644 index 0000000000..8034471960 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields/ConductorArrayMapForm.tsx @@ -0,0 +1,143 @@ +import { Fragment, FunctionComponent } from "react"; +import { Box, Grid, IconButton } from "@mui/material"; +import { Button } from "components"; +import maybeVariable from "../maybeVariableHOC"; +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import { ConductorEmptyGroupField } from "components/v1/ConductorEmptyGroupField"; +import AddIcon from "components/v1/icons/AddIcon"; +import TrashIcon from "components/v1/icons/TrashIcon"; + +const ROLE_SUGGESTION = ["user", "assistant", "system", "human"]; + +interface ConductorArrayMapFormFieldProps { + availableOptions: string[]; + onChange: (idx: number, role: string, message: string) => void; + idx: number; + data: { role: string; message: string }; + handleRemoveItem: (idx: number) => void; +} + +const ConductorArrayMapFormField: FunctionComponent< + ConductorArrayMapFormFieldProps +> = ({ availableOptions, onChange, idx, data, handleRemoveItem }) => { + return ( + + + { + onChange(idx, selectedKey, data.message); + }} + otherOptions={availableOptions} + value={data.role ?? ""} + label="Role" + /> + + + { + onChange(idx, data.role, val); + }} + value={data.message ?? ""} + label="Message" + /> + + + handleRemoveItem(idx)}> + + + + + ); +}; + +interface ConductorArrayMapFormProps { + value: { role: string; message: string }[]; + onChange: (messages: { role: string; message: string }[]) => void; +} + +const ConductorArrayMapFormBase: FunctionComponent< + ConductorArrayMapFormProps +> = ({ value, onChange }) => { + const handleAddItem = () => { + const newMessages = [...value, { role: "", message: "" }]; + onChange(newMessages); + }; + + const handleRemoveItem = (idx: number) => { + const newMessages = [...value]; + newMessages.splice(idx, 1); + onChange(newMessages); + }; + + const handleChangeItem = (idx: number, role: string, message: string) => { + const newMessages = [...value]; + newMessages[idx] = { role, message }; + onChange(newMessages); + }; + + return ( + <> + {(!value || value.length === 0) && ( + + )} + {Array.isArray(value) && value.length > 0 && ( + <> + + {value.map((item, idx) => ( + + + + ))} + + + + )} + + ); +}; + +const ConductorArrayMapForm = maybeVariable(ConductorArrayMapFormBase); +export { ConductorArrayMapForm, ConductorArrayMapFormBase }; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields/LLMFormFields.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields/LLMFormFields.tsx new file mode 100644 index 0000000000..1cbc392072 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields/LLMFormFields.tsx @@ -0,0 +1,70 @@ +import { Grid } from "@mui/material"; +import { TaskDef } from "types"; +import { UiIntegrationsFieldType } from "types/FormFieldTypes"; +import { FieldComponentType } from "utils/fieldHelpers"; +import { ActorRef } from "xstate"; +import { LLMFormFieldsEvents } from "./state"; + +interface LLMFormFieldsProps { + onChange: (task: Partial) => void; + task: Partial; + fieldFieldComponents: Array<[UiIntegrationsFieldType, FieldComponentType]>; + actor: ActorRef; +} + +const sizeMap = (type: UiIntegrationsFieldType) => { + if ( + [ + UiIntegrationsFieldType.PROMPT_NAME, + UiIntegrationsFieldType.VECTOR_DB, + UiIntegrationsFieldType.MESSAGES, + UiIntegrationsFieldType.INSTRUCTIONS, + UiIntegrationsFieldType.JSON_OUTPUT, + UiIntegrationsFieldType.STOP_WORDS, + ].includes(type) + ) { + return 12; + } + if ( + [ + UiIntegrationsFieldType.TEMPERATURE, + UiIntegrationsFieldType.TOP_P, + ].includes(type) + ) { + return 3; + } + return 6; +}; + +export const LLMFormFields = ({ + fieldFieldComponents, + onChange, + task, + actor, +}: LLMFormFieldsProps) => { + return ( + + {fieldFieldComponents.map(([type, FieldComponent]) => { + return ( + + + + ); + })} + + ); +}; + +export type { LLMFormFieldsProps }; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields/LLMFormFieldsWrapper.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields/LLMFormFieldsWrapper.tsx new file mode 100644 index 0000000000..59fd55e490 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields/LLMFormFieldsWrapper.tsx @@ -0,0 +1,128 @@ +import React from "react"; +import { useInterpret } from "@xstate/react"; +import { TaskDef } from "types"; +import { UiIntegrationsFieldType } from "types/FormFieldTypes"; +import { FieldComponentType, updateField } from "utils/fieldHelpers"; +import { useAuthHeaders } from "utils/query"; +import { ActorRef } from "xstate"; +import { + LLMFormFieldsEvents, + LLMFormFieldsMachineContext, + SelectInstructionsEvent, + SelectPromptNameEvent, + llmFormFieldsMachine, +} from "./state"; + +interface LLMFormFieldsWrapperProps { + onChange: (task: Partial) => void; + task: Partial; + allFieldComponents: Array<[UiIntegrationsFieldType, FieldComponentType]>; + children: (actor: ActorRef) => React.ReactNode; +} + +const LLMFormFieldsWrapper = ({ + onChange, + task, + allFieldComponents, + children, +}: LLMFormFieldsWrapperProps) => { + const authHeaders = useAuthHeaders(); + const fields = allFieldComponents?.map(([type]) => type); + const actor = useInterpret(llmFormFieldsMachine, { + ...(process.env.NODE_ENV === "development" ? { devTools: true } : {}), + context: { + authHeaders, + fields, + task, + }, + actions: { + selectPromptName: ( + ctx: LLMFormFieldsMachineContext, + event: SelectPromptNameEvent, + ) => { + const maybeAvailablePromptName = ctx.promptNameOptions.find( + ({ name }) => name === event?.task?.inputParameters?.promptName, + ); + if (maybeAvailablePromptName) { + const newVariables = Object.fromEntries( + (maybeAvailablePromptName?.variables as string[]).map((l) => [ + l, + "", + ]), + ); + + const resultVariables = { + ...newVariables, + }; + + const taskWithVariables = updateField( + `inputParameters.promptVariables`, + resultVariables, + event.task, + ); + + const taskWithSelectedPromptName = updateField( + `inputParameters.${UiIntegrationsFieldType.PROMPT_NAME}`, + maybeAvailablePromptName?.name, + taskWithVariables, + ); + + onChange(taskWithSelectedPromptName); + } else { + const updatedTask = updateField( + `inputParameters.${UiIntegrationsFieldType.PROMPT_NAME}`, + event?.task?.inputParameters?.promptName, + event.task, + ); + + onChange(updatedTask); + } + }, + selectInstructions: ( + ctx: LLMFormFieldsMachineContext, + event: SelectInstructionsEvent, + ) => { + const maybeAvailablePromptName = ctx.promptNameOptions.find( + ({ name }) => name === event?.task?.inputParameters?.instructions, + ); + if (maybeAvailablePromptName) { + const newVariables = Object.fromEntries( + (maybeAvailablePromptName?.variables as string[]).map((l) => [ + l, + "", + ]), + ); + + const resultVariables = { + ...newVariables, + }; + + const taskWithVariables = updateField( + `inputParameters.promptVariables`, + resultVariables, + event.task, + ); + + const taskWithSelectedPromptName = updateField( + `inputParameters.${UiIntegrationsFieldType.INSTRUCTIONS}`, + maybeAvailablePromptName?.name, + taskWithVariables, + ); + + onChange(taskWithSelectedPromptName); + } else { + const updatedTask = updateField( + `inputParameters.${UiIntegrationsFieldType.INSTRUCTIONS}`, + event?.task?.inputParameters?.instructions, + event.task, + ); + + onChange(updatedTask); + } + }, + }, + }); + return <>{children(actor)}; +}; + +export default LLMFormFieldsWrapper; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields/index.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields/index.ts new file mode 100644 index 0000000000..f410f2fbcf --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields/index.ts @@ -0,0 +1 @@ +export * from "./LLMFormFields"; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields/state/actions.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields/state/actions.ts new file mode 100644 index 0000000000..084f554b5a --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields/state/actions.ts @@ -0,0 +1,51 @@ +import { assign, DoneInvokeEvent } from "xstate"; +import { LLMFormFieldsMachineContext } from "./types"; + +export const persistLlmProviderOptions = assign< + LLMFormFieldsMachineContext, + DoneInvokeEvent +>({ + llmProviderOptions: (_, { data }) => data, +}); + +export const persistModelOptions = assign< + LLMFormFieldsMachineContext, + DoneInvokeEvent +>({ + modelOptions: (_, { data }) => data, +}); + +export const persistPromptNameOptions = assign< + LLMFormFieldsMachineContext, + DoneInvokeEvent +>({ + promptNameOptions: (_, { data }) => data, +}); + +export const persistVectorDbOptions = assign< + LLMFormFieldsMachineContext, + DoneInvokeEvent +>({ + vectorDbOptions: (_, { data }) => data, +}); + +export const persistEmbeddingModelOptions = assign< + LLMFormFieldsMachineContext, + DoneInvokeEvent +>({ + embeddingModelOptions: (_, { data }) => data, +}); + +export const persistIndexesOptions = assign< + LLMFormFieldsMachineContext, + DoneInvokeEvent +>({ + indexOptions: (_, { data }) => data, +}); + +export const persistError = assign< + LLMFormFieldsMachineContext, + DoneInvokeEvent +>({ + error: (_, { data }) => ({ message: data, severity: "error" }), +}); diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields/state/index.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields/state/index.ts new file mode 100644 index 0000000000..79fd82215f --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields/state/index.ts @@ -0,0 +1,2 @@ +export * from "./machine"; +export * from "./types"; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields/state/machine.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields/state/machine.ts new file mode 100644 index 0000000000..eba2a0c288 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields/state/machine.ts @@ -0,0 +1,149 @@ +import { createMachine } from "xstate"; +import { + LLMFormFieldsMachineContext, + LLMFormFieldsEvents, + LLMFormFieldsMachineStates, + LLMFormFieldsMachineEventTypes, +} from "./types"; +import * as services from "./services"; +import * as actions from "./actions"; +import { UiIntegrationsFieldType } from "types/FormFieldTypes"; + +export const llmFormFieldsMachine = createMachine< + LLMFormFieldsMachineContext, + LLMFormFieldsEvents +>( + { + id: "llmFormFieldsMachine", + predictableActionArguments: true, + initial: LLMFormFieldsMachineStates.DETERMINE_INITIAL_STATE, + context: { + task: {}, + fields: [], + llmProviderOptions: [], + modelOptions: [], + promptNameOptions: [], + vectorDbOptions: [], + indexOptions: [], + embeddingModelOptions: [], + selectedPromptName: undefined, + }, + states: { + [LLMFormFieldsMachineStates.DETERMINE_INITIAL_STATE]: { + always: [ + { + target: LLMFormFieldsMachineStates.FETCH_VECTORDB_OPTIONS, + cond: (context) => + context.fields.some( + (field) => field === UiIntegrationsFieldType.VECTOR_DB, + ), + }, + { + target: LLMFormFieldsMachineStates.FETCH_LLM_PROVIDER_OPTIONS, + cond: (context) => + context.fields.some( + (field) => field === UiIntegrationsFieldType.LLM_PROVIDER, + ), + }, + ], + }, + [LLMFormFieldsMachineStates.IDLE]: { + on: { + [LLMFormFieldsMachineEventTypes.FOCUS_LLM_PROVIDER]: [ + { + target: LLMFormFieldsMachineStates.FETCH_LLM_PROVIDER_OPTIONS, + cond: (context: LLMFormFieldsMachineContext) => + context.llmProviderOptions.length === 0, + }, + { target: LLMFormFieldsMachineStates.IDLE }, + ], + [LLMFormFieldsMachineEventTypes.FOCUS_EMBEDDINGS_MODEL]: + LLMFormFieldsMachineStates.FETCH_EMBEDDINGS_MODEL, + [LLMFormFieldsMachineEventTypes.FOCUS_INDEX]: + LLMFormFieldsMachineStates.FETCH_INDEX_OPTIONS, + [LLMFormFieldsMachineEventTypes.FOCUS_PROMPT_NAMES]: + LLMFormFieldsMachineStates.FETCH_PROMPT_NAMES, + [LLMFormFieldsMachineEventTypes.FOCUS_VECTORDB]: [ + { + target: LLMFormFieldsMachineStates.FETCH_VECTORDB_OPTIONS, + cond: (context: LLMFormFieldsMachineContext) => + context.vectorDbOptions.length === 0, + }, + { target: LLMFormFieldsMachineStates.IDLE }, + ], + [LLMFormFieldsMachineEventTypes.FOCUS_MODEL]: + LLMFormFieldsMachineStates.FETCH_MODEL_OPTIONS, + [LLMFormFieldsMachineEventTypes.SELECT_PROMPT_NAME]: { + actions: "selectPromptName", + }, + [LLMFormFieldsMachineEventTypes.SELECT_INSTRUCTIONS]: { + actions: "selectInstructions", + }, + }, + }, + [LLMFormFieldsMachineStates.FETCH_LLM_PROVIDER_OPTIONS]: { + invoke: { + src: "fetchLlmProviderOptionsService", + onDone: { + actions: "persistLlmProviderOptions", + target: LLMFormFieldsMachineStates.IDLE, + }, + }, + }, + [LLMFormFieldsMachineStates.FETCH_MODEL_OPTIONS]: { + invoke: { + src: "fetchForModels", + onDone: { + actions: "persistModelOptions", + target: LLMFormFieldsMachineStates.IDLE, + }, + onError: LLMFormFieldsMachineStates.IDLE, + }, + }, + [LLMFormFieldsMachineStates.FETCH_VECTORDB_OPTIONS]: { + invoke: { + src: "fetchForVectorDb", + onDone: { + actions: "persistVectorDbOptions", + target: LLMFormFieldsMachineStates.IDLE, + }, + onError: LLMFormFieldsMachineStates.IDLE, + }, + }, + [LLMFormFieldsMachineStates.FETCH_PROMPT_NAMES]: { + invoke: { + src: "fetchForPromptNames", + onDone: { + actions: "persistPromptNameOptions", + target: LLMFormFieldsMachineStates.IDLE, + }, + onError: LLMFormFieldsMachineStates.IDLE, + }, + }, + [LLMFormFieldsMachineStates.FETCH_INDEX_OPTIONS]: { + invoke: { + src: "fetchForIndexes", + onDone: { + actions: "persistIndexesOptions", + target: LLMFormFieldsMachineStates.IDLE, + }, + onError: LLMFormFieldsMachineStates.IDLE, + }, + }, + [LLMFormFieldsMachineStates.FETCH_EMBEDDINGS_MODEL]: { + invoke: { + src: "fetchForEmbeddingModel", + onDone: { + actions: "persistEmbeddingModelOptions", + target: LLMFormFieldsMachineStates.IDLE, + }, + onError: LLMFormFieldsMachineStates.IDLE, + }, + }, + }, + }, + { + actions: actions as any, + services: services as any, + }, +); diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields/state/services.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields/state/services.ts new file mode 100644 index 0000000000..a9601ae45d --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields/state/services.ts @@ -0,0 +1,141 @@ +import { queryClient } from "queryClient"; +import { fetchWithContext, fetchContextNonHook } from "plugins/fetch"; +import { LLMFormFieldsMachineContext, FocusEvent } from "./types"; +import { logger } from "utils/logger"; +import { IntegrationCategory } from "types/Integrations"; + +const fetchContext = fetchContextNonHook(); + +export const fetchLlmProviderOptionsService = async ({ + authHeaders: headers, +}: LLMFormFieldsMachineContext) => { + const path = `/integrations/provider?category=${IntegrationCategory.AI_MODEL}&activeOnly=true`; + try { + const response = await queryClient.fetchQuery( + [fetchContext.stack, path], + () => fetchWithContext(path, fetchContext, { headers }), + ); + return response; + } catch (error) { + logger.error(error); + return []; + } +}; + +export const fetchForModels = async ( + { authHeaders: headers }: LLMFormFieldsMachineContext, + { task }: FocusEvent, +) => { + const maybeLlmProvider = task?.inputParameters?.llmProvider; + if (maybeLlmProvider) { + const path = `/integrations/provider/${maybeLlmProvider}/integration?activeOnly=true`; + try { + const response = await queryClient.fetchQuery( + [fetchContext.stack, path], + () => fetchWithContext(path, fetchContext, { headers }), + ); + return response; + } catch (error) { + logger.error(error); + return []; + } + } + return []; +}; + +export const fetchForPromptNames = async ( + { authHeaders: headers }: LLMFormFieldsMachineContext, + { task }: FocusEvent, +) => { + const maybeLlmProvider = task?.inputParameters?.llmProvider; + const maybeModel = task?.inputParameters?.model; + if (maybeModel && maybeLlmProvider) { + const path = `/integrations/provider/${maybeLlmProvider}/integration/${maybeModel}/prompt`; + try { + const response = await queryClient.fetchQuery( + [fetchContext.stack, path], + () => fetchWithContext(path, fetchContext, { headers }), + ); + return response; + } catch (error) { + logger.error(error); + return []; + } + } + return []; +}; + +export const fetchForVectorDb = async ({ + authHeaders: headers, +}: LLMFormFieldsMachineContext) => { + const path = `/integrations/provider?category=${IntegrationCategory.VECTOR_DB}&activeOnly=true`; + try { + const response = await queryClient.fetchQuery( + [fetchContext.stack, path], + () => fetchWithContext(path, fetchContext, { headers }), + ); + return response; + } catch (error) { + logger.error(error); + return []; + } +}; + +export const fetchForIndexes = async ( + { authHeaders: headers }: LLMFormFieldsMachineContext, + { task }: FocusEvent, +) => { + const maybeVectorDB = task?.inputParameters?.vectorDB; + if (maybeVectorDB) { + const path = `/integrations/provider/${maybeVectorDB}/integration?activeOnly=true`; + try { + const response = await queryClient.fetchQuery( + [fetchContext.stack, path], + () => fetchWithContext(path, fetchContext, { headers }), + ); + return response; + } catch (error) { + logger.error(error); + return []; + } + } + return []; +}; + +export const fetchForEmbeddingsModelProvider = async ({ + authHeaders: headers, +}: LLMFormFieldsMachineContext) => { + const path = `/integrations/provider?category=${IntegrationCategory.AI_MODEL}&activeOnly=true`; + try { + const response = await queryClient.fetchQuery( + [fetchContext.stack, path], + () => fetchWithContext(path, fetchContext, { headers }), + ); + return response; + } catch (error) { + logger.error(error); + return []; + } +}; + +export const fetchForEmbeddingModel = async ( + { authHeaders: headers }: LLMFormFieldsMachineContext, + { task }: FocusEvent, +) => { + const maybeEmbeddingModelProvider = + task?.inputParameters?.embeddingModelProvider; + if (maybeEmbeddingModelProvider) { + const path = `/integrations/provider/${maybeEmbeddingModelProvider}/integration?activeOnly=true`; + try { + const response = await queryClient.fetchQuery( + [fetchContext.stack, path], + () => fetchWithContext(path, fetchContext, { headers }), + ); + return response; + } catch (error) { + logger.error(error); + return []; + } + } + return []; +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields/state/types.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields/state/types.ts new file mode 100644 index 0000000000..b892ec66da --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields/state/types.ts @@ -0,0 +1,72 @@ +import { AuthHeaders } from "types/common"; +import { PromptDef, TaskDef, UiIntegrationsFieldType } from "types"; + +export enum LLMFormFieldsMachineEventTypes { + FOCUS_LLM_PROVIDER = "FOCUS_LLM_PROVIDER", + FOCUS_PROMPT_NAMES = "FOCUS_PROMPT_NAME", + FOCUS_MODEL = "FOCUS_MODEL", + FOCUS_VECTORDB = "FOCUS_VECTORDB", + FOCUS_INDEX = "FOCUS_INDEX", + FOCUS_EMBEDDINGS_MODEL_PROVIDER = "FOCUS_EMBEDDINGS_MODEL_PROVIDER", + FOCUS_EMBEDDINGS_MODEL = "FOCUS_EMBEDDINGS_MODEL", + + SELECT_PROMPT_NAME = "SELECT_PROMPT_NAME", + UPDATE_TASK = "UPDATE_TASK", + SELECT_INSTRUCTIONS = "SELECT_INSTRUCTIONS", +} + +export enum LLMFormFieldsMachineStates { + DETERMINE_INITIAL_STATE = "DETERMINE_INITIAL_STATE", + IDLE = "IDLE", + FETCH_MODEL_OPTIONS = "FETCH_MODEL_OPTIONS", + FETCH_PROMPT_NAMES = "FETCH_PROMPT_NAME", + FETCH_LLM_PROVIDER_OPTIONS = "FETCH_LLM_PROVIDER_OPTIONS", + FETCH_VECTORDB_OPTIONS = "FETCH_VECTORDB_OPTIONS", + FETCH_INDEX_OPTIONS = "FETCH_INDEX_OPTIONS", + FETCH_EMBEDDINGS_MODEL_PROVIDER = "FETCH_EMBEDDINGS_MODEL_PROVIDER", + FETCH_EMBEDDINGS_MODEL = "FETCH_EMBEDDINGS_MODEL", +} + +export type FocusEvent = { + type: LLMFormFieldsMachineEventTypes; + task: Partial; +}; + +export type UpdateTaskEvent = { + type: LLMFormFieldsMachineEventTypes; + task: Partial; +}; + +export type SelectPromptNameEvent = { + type: LLMFormFieldsMachineEventTypes.SELECT_PROMPT_NAME; + task: Partial; +}; + +export type SelectInstructionsEvent = { + type: LLMFormFieldsMachineEventTypes.SELECT_INSTRUCTIONS; + task: Partial; +}; + +type Error = { + message: string; + severity: string; // Not really a string +}; + +export type LLMFormFieldsEvents = + | FocusEvent + | SelectPromptNameEvent + | UpdateTaskEvent; + +export interface LLMFormFieldsMachineContext { + fields: UiIntegrationsFieldType[]; + selectedPromptName?: Record; + authHeaders?: AuthHeaders; + llmProviderOptions: []; + promptNameOptions: PromptDef[]; + modelOptions: []; + vectorDbOptions: []; + indexOptions: []; + embeddingModelOptions: []; + error?: Error; + task: Partial; +} diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMGenerateEmbeddingsTaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMGenerateEmbeddingsTaskForm.tsx new file mode 100644 index 0000000000..c240fd754f --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMGenerateEmbeddingsTaskForm.tsx @@ -0,0 +1,71 @@ +import { Box } from "@mui/material"; +import { LLMFormFields } from "pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields"; +import { UiIntegrationsFieldType } from "types/FormFieldTypes"; +import { fieldsToFieldsFieldsComponents } from "utils/fieldHelpers"; +import { ConductorCacheOutput } from "./ConductorCacheOutputForm"; +import { Optional } from "./OptionalFieldForm"; +import TaskFormSection from "./TaskFormSection"; +import { TaskFormProps } from "./types"; +import LLMFormFieldsWrapper from "./LLMFormFields/LLMFormFieldsWrapper"; + +const modelFields = [ + UiIntegrationsFieldType.LLM_PROVIDER, + UiIntegrationsFieldType.MODEL, +]; + +const embeddingFields = [ + UiIntegrationsFieldType.TEXT, + UiIntegrationsFieldType.DIMENSIONS, +]; + +const modelFieldComponents = fieldsToFieldsFieldsComponents(modelFields); +const embeddingFieldComponents = + fieldsToFieldsFieldsComponents(embeddingFields); + +const allFieldComponents = [ + ...modelFieldComponents, + ...embeddingFieldComponents, +]; + +export const LLMGenerateEmbeddingsTaskForm = ({ + task, + onChange, +}: TaskFormProps) => { + return ( + + {(actor) => ( + + + + + + + + + + + + + + + )} + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMGetEmbeddingsTaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMGetEmbeddingsTaskForm.tsx new file mode 100644 index 0000000000..406c0f72f6 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMGetEmbeddingsTaskForm.tsx @@ -0,0 +1,66 @@ +import { Box } from "@mui/material"; +import { LLMFormFields } from "pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields"; +import { UiIntegrationsFieldType } from "types/FormFieldTypes"; +import { fieldsToFieldsFieldsComponents } from "utils/fieldHelpers"; +import { ConductorCacheOutput } from "./ConductorCacheOutputForm"; +import { Optional } from "./OptionalFieldForm"; +import TaskFormSection from "./TaskFormSection"; +import { TaskFormProps } from "./types"; +import LLMFormFieldsWrapper from "./LLMFormFields/LLMFormFieldsWrapper"; + +const vectorDbFields = [ + UiIntegrationsFieldType.VECTOR_DB, + UiIntegrationsFieldType.NAMESPACE, + UiIntegrationsFieldType.INDEX, +]; + +const embeddingFields = [UiIntegrationsFieldType.EMBEDDINGS]; + +const vectorDbFieldComponents = fieldsToFieldsFieldsComponents(vectorDbFields); +const embeddingFieldComponents = + fieldsToFieldsFieldsComponents(embeddingFields); + +const allFieldComponents = [ + ...vectorDbFieldComponents, + ...embeddingFieldComponents, +]; + +export const LLMGetEmbeddingsTaskForm = ({ task, onChange }: TaskFormProps) => { + return ( + + {(actor) => ( + + + + + + + + + + + + + + + )} + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMIndexDocumentTaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMIndexDocumentTaskForm.tsx new file mode 100644 index 0000000000..245a9fa5a9 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMIndexDocumentTaskForm.tsx @@ -0,0 +1,100 @@ +import { Box } from "@mui/material"; +import { LLMFormFields } from "pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields"; +import { UiIntegrationsFieldType } from "types/FormFieldTypes"; +import { fieldsToFieldsFieldsComponents } from "utils/fieldHelpers"; +import { ConductorCacheOutput } from "./ConductorCacheOutputForm"; +import { Optional } from "./OptionalFieldForm"; +import TaskFormSection from "./TaskFormSection"; +import { TaskFormProps } from "./types"; +import LLMFormFieldsWrapper from "./LLMFormFields/LLMFormFieldsWrapper"; + +const vectorDbFields = [ + UiIntegrationsFieldType.VECTOR_DB, + UiIntegrationsFieldType.INDEX, + UiIntegrationsFieldType.NAMESPACE, +]; + +const embeddingModelFields = [ + UiIntegrationsFieldType.EMBEDDING_MODEL_PROVIDER, + UiIntegrationsFieldType.EMBEDDING_MODEL, + UiIntegrationsFieldType.DIMENSIONS, +]; + +const documentFields = [ + UiIntegrationsFieldType.URL, + UiIntegrationsFieldType.MEDIA_TYPE, +]; + +const chunkingFields = [ + UiIntegrationsFieldType.CHUNK_SIZE, + UiIntegrationsFieldType.CHUNK_OVERLAP, +]; + +const vectorDbFieldComponents = fieldsToFieldsFieldsComponents(vectorDbFields); +const embeddingModelFieldComponents = + fieldsToFieldsFieldsComponents(embeddingModelFields); +const documentFieldComponents = fieldsToFieldsFieldsComponents(documentFields); +const chunkingFieldComponents = fieldsToFieldsFieldsComponents(chunkingFields); + +const allFieldComponents = [ + ...vectorDbFieldComponents, + ...embeddingModelFieldComponents, + ...documentFieldComponents, + ...chunkingFieldComponents, +]; + +export const LLMIndexDocumentTaskForm = ({ task, onChange }: TaskFormProps) => { + return ( + + {(actor) => ( + + + + + + + + + + + + + + + + + + + + + )} + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMIndexTextTaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMIndexTextTaskForm.tsx new file mode 100644 index 0000000000..6fb377d086 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMIndexTextTaskForm.tsx @@ -0,0 +1,86 @@ +import { Box } from "@mui/material"; + +import { LLMFormFields } from "pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields"; +import { UiIntegrationsFieldType } from "types/FormFieldTypes"; +import { fieldsToFieldsFieldsComponents } from "utils/fieldHelpers"; +import { ConductorCacheOutput } from "./ConductorCacheOutputForm"; +import { Optional } from "./OptionalFieldForm"; +import TaskFormSection from "./TaskFormSection"; +import { TaskFormProps } from "./types"; +import LLMFormFieldsWrapper from "./LLMFormFields/LLMFormFieldsWrapper"; + +const vectorDbFields = [ + UiIntegrationsFieldType.VECTOR_DB, + UiIntegrationsFieldType.NAMESPACE, + UiIntegrationsFieldType.INDEX, +]; + +const embeddingModelFields = [ + UiIntegrationsFieldType.EMBEDDING_MODEL_PROVIDER, + UiIntegrationsFieldType.EMBEDDING_MODEL, + UiIntegrationsFieldType.DIMENSIONS, +]; + +const textFields = [ + UiIntegrationsFieldType.TEXT, + UiIntegrationsFieldType.DOC_ID, +]; + +const vectorDbFieldComponents = fieldsToFieldsFieldsComponents(vectorDbFields); +const embeddingModelFieldComponents = + fieldsToFieldsFieldsComponents(embeddingModelFields); +const textFieldComponents = fieldsToFieldsFieldsComponents(textFields); + +const allFieldComponents = [ + ...vectorDbFieldComponents, + ...embeddingModelFieldComponents, + ...textFieldComponents, +]; + +export const LLMIndexTextTaskForm = ({ task, onChange }: TaskFormProps) => { + return ( + + {(actor) => ( + + + + + + + + + + + + + + + + + + )} + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMSearchIndexTaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMSearchIndexTaskForm.tsx new file mode 100644 index 0000000000..90c2f7563d --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMSearchIndexTaskForm.tsx @@ -0,0 +1,83 @@ +import { Box } from "@mui/material"; +import { LLMFormFields } from "pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields"; +import { UiIntegrationsFieldType } from "types/FormFieldTypes"; +import { fieldsToFieldsFieldsComponents } from "utils/fieldHelpers"; +import { ConductorCacheOutput } from "./ConductorCacheOutputForm"; +import { Optional } from "./OptionalFieldForm"; +import TaskFormSection from "./TaskFormSection"; +import { TaskFormProps } from "./types"; +import LLMFormFieldsWrapper from "./LLMFormFields/LLMFormFieldsWrapper"; + +const vectorDbFields = [ + UiIntegrationsFieldType.VECTOR_DB, + UiIntegrationsFieldType.INDEX, + UiIntegrationsFieldType.NAMESPACE, +]; + +const embeddingModelFields = [ + UiIntegrationsFieldType.EMBEDDING_MODEL_PROVIDER, + UiIntegrationsFieldType.EMBEDDING_MODEL, +]; + +const searchFields = [ + UiIntegrationsFieldType.QUERY, + UiIntegrationsFieldType.MAX_RESULTS, + UiIntegrationsFieldType.DIMENSIONS, +]; + +const vectorDbFieldComponents = fieldsToFieldsFieldsComponents(vectorDbFields); +const embeddingModelFieldComponents = + fieldsToFieldsFieldsComponents(embeddingModelFields); +const searchFieldComponents = fieldsToFieldsFieldsComponents(searchFields); + +const allFieldComponents = [ + ...vectorDbFieldComponents, + ...embeddingModelFieldComponents, + ...searchFieldComponents, +]; + +export const LLMSearchIndexTaskForm = ({ task, onChange }: TaskFormProps) => ( + + {(actor) => ( + + + + + + + + + + + + + + + + + + )} + +); diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMStoreEmbeddingsTaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMStoreEmbeddingsTaskForm.tsx new file mode 100644 index 0000000000..2352b74c1a --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMStoreEmbeddingsTaskForm.tsx @@ -0,0 +1,90 @@ +import { Box } from "@mui/material"; +import { LLMFormFields } from "pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields"; +import { UiIntegrationsFieldType } from "types/FormFieldTypes"; +import { fieldsToFieldsFieldsComponents } from "utils/fieldHelpers"; +import { ConductorCacheOutput } from "./ConductorCacheOutputForm"; +import { Optional } from "./OptionalFieldForm"; +import TaskFormSection from "./TaskFormSection"; +import { TaskFormProps } from "./types"; +import { ConductorFlatMapFormBase } from "components/v1/FlatMapForm/ConductorFlatMapForm"; +import LLMFormFieldsWrapper from "./LLMFormFields/LLMFormFieldsWrapper"; + +const vectorDbFields = [ + UiIntegrationsFieldType.VECTOR_DB, + UiIntegrationsFieldType.INDEX, + UiIntegrationsFieldType.NAMESPACE, + UiIntegrationsFieldType.ID, +]; + +const embeddingModelFields = [ + UiIntegrationsFieldType.EMBEDDING_MODEL_PROVIDER, + UiIntegrationsFieldType.EMBEDDING_MODEL, + UiIntegrationsFieldType.EMBEDDINGS, +]; + +const vectorDbFieldComponents = fieldsToFieldsFieldsComponents(vectorDbFields); +const embeddingModelFieldComponents = + fieldsToFieldsFieldsComponents(embeddingModelFields); + +const allFieldComponents = [ + ...vectorDbFieldComponents, + ...embeddingModelFieldComponents, +]; + +export const LLMStoreEmbeddingsTaskForm = ({ + task, + onChange, +}: TaskFormProps) => ( + + {(actor) => ( + + + + + + + + + + onChange({ + ...task, + inputParameters: { + ...(task?.inputParameters || {}), + metadata: newParams, + }, + }) + } + /> + + + + + + + + + )} + +); diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMTextCompleteTaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMTextCompleteTaskForm.tsx new file mode 100644 index 0000000000..26ac74a24d --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/LLMTextCompleteTaskForm.tsx @@ -0,0 +1,82 @@ +import { Box } from "@mui/material"; +import { LLMFormFields } from "pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields"; +import { UiIntegrationsFieldType } from "types/FormFieldTypes"; +import { fieldsToFieldsFieldsComponents } from "utils/fieldHelpers"; +import { ConductorCacheOutput } from "./ConductorCacheOutputForm"; +import { Optional } from "./OptionalFieldForm"; +import TaskFormSection from "./TaskFormSection"; +import { TaskFormProps } from "./types"; +import LLMFormFieldsWrapper from "./LLMFormFields/LLMFormFieldsWrapper"; + +const modelFields = [ + UiIntegrationsFieldType.LLM_PROVIDER, + UiIntegrationsFieldType.MODEL, +]; + +const promptFields = [UiIntegrationsFieldType.PROMPT_NAME]; + +const fineTuningFields = [ + UiIntegrationsFieldType.TEMPERATURE, + UiIntegrationsFieldType.TOP_P, + UiIntegrationsFieldType.MAX_TOKENS, + UiIntegrationsFieldType.STOP_WORDS, +]; + +const modelFieldComponents = fieldsToFieldsFieldsComponents(modelFields); +const promptFieldComponents = fieldsToFieldsFieldsComponents(promptFields); +const fineTuningFieldComponents = + fieldsToFieldsFieldsComponents(fineTuningFields); + +const allFieldComponents = [ + ...modelFieldComponents, + ...promptFieldComponents, + ...fineTuningFieldComponents, +]; + +export const LLMTextCompleteTaskForm = ({ task, onChange }: TaskFormProps) => { + return ( + + {(actor) => ( + + + + + + + + + + + + + + + + + + )} + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/ListFilesTaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/ListFilesTaskForm.tsx new file mode 100644 index 0000000000..e666381fde --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/ListFilesTaskForm.tsx @@ -0,0 +1,305 @@ +import { Box, Grid, Typography } from "@mui/material"; +import { path as _path, pipe as _pipe, assoc as _assoc } from "lodash/fp"; +import { useState } from "react"; + +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import { ConductorFlatMapForm } from "components/v1/FlatMapForm/ConductorFlatMapForm"; +import { TaskType } from "types"; +import { updateField } from "utils/fieldHelpers"; +import TaskFormSection from "./TaskFormSection"; +import { TaskFormProps } from "./types"; +import { Optional } from "./OptionalFieldForm"; +import { ConductorAutoComplete } from "components/v1"; +import { useGetIntegration } from "utils/hooks"; +import { MaybeVariable } from "./MaybeVariable"; + +const DEFAULT_VALUES_FOR_FILE_TYPES_ARRAY = [ + "java", + "xls", + "csv", + "pdf", + "All", +]; +const integrationNamePath = "inputParameters.integrationName"; +const inputLocationPath = "inputParameters.inputLocation"; +const outputLocationPath = "inputParameters.outputLocation"; +const fileTypesPath = "inputParameters.fileTypes"; +const integrationNamesPath = "inputParameters.integrationNames"; + +// Helper function to validate URL format for web-based inputs +const validateWebUrl = (value: string): string | null => { + // Skip validation for variable references + if (value.includes("${") || value.includes("$.")) { + return null; + } + + // Check for cloud storage protocols first + if (value.startsWith("s3://")) { + // Validate S3 URL format: s3://bucketname/folder + const s3Path = value.substring(5); // Remove "s3://" + if (!s3Path || s3Path.length === 0) { + return "Invalid S3 URL: Missing bucket name. Example: s3://bucketname/folder"; + } + if (s3Path.includes("//")) { + return "Invalid S3 URL: Double slashes not allowed. Example: s3://bucketname/folder"; + } + return null; + } + + if (value.startsWith("gs://")) { + // Validate Google Cloud Storage URL format: gs://path + const gsPath = value.substring(5); // Remove "gs://" + if (!gsPath || gsPath.length === 0) { + return "Invalid GCS URL: Missing path. Example: gs://path"; + } + if (gsPath.includes("//")) { + return "Invalid GCS URL: Double slashes not allowed. Example: gs://path"; + } + return null; + } + + if (value.startsWith("azureblob://")) { + // Validate Azure Blob Storage URL format: azureblob://path + const azurePath = value.substring(12); // Remove "azureblob://" + if (!azurePath || azurePath.length === 0) { + return "Invalid Azure Blob URL: Missing path. Example: azureblob://path"; + } + if (azurePath.includes("//")) { + return "Invalid Azure Blob URL: Double slashes not allowed. Example: azureblob://path"; + } + return null; + } + + // Check if it's a web-based URL (http/https) + try { + const url = new URL(value); + // Validate that the URL has a valid hostname + if (!url.hostname || url.hostname.length === 0) { + return "Invalid URL: Missing hostname"; + } + // Check for proper URL structure + if (url.protocol !== "http:" && url.protocol !== "https:") { + return "Invalid URL: Only http://, https://, s3://, gs://, and azureblob:// protocols are supported"; + } + } catch { + return "Invalid URL format. Examples: https://example.com/path, s3://bucketname/folder, gs://bucketname/folder, azureblob://container/path"; + } + + return null; +}; + +export const ListFilesTaskForm = ({ task, onChange }: TaskFormProps) => { + const integrationName = _path(integrationNamePath, task); + const inputLocation = _path(inputLocationPath, task); + const outputLocation = _path(outputLocationPath, task); + const fileTypes = _path(fileTypesPath, task); + const integrationNames = _path(integrationNamesPath, task); + + const [inputLocationError, setInputLocationError] = useState( + null, + ); + + // need to fetch compatible integration names and pass them to the integrationName autocomplete options + const integrations = useGetIntegration({}); + + // Filter integrations to only include git, aws, and gcp types + const integrationNameOptions = + integrations?.data + ?.filter((integration) => { + const type = integration?.type?.toLowerCase() || ""; + // Filter by type containing git, aws, or gcp + return type === "git" || type === "aws" || type === "gcp"; + }) + ?.map((integration) => integration?.name) || []; + + return ( + + + + + + + + onChange(updateField(integrationNamePath, changes, task)) + } + /> + + + + + + + { + // Validate URL format for web-based inputs + const error = validateWebUrl(changes); + setInputLocationError(error); + onChange(updateField(inputLocationPath, changes, task)); + }} + label="Input Location" + error={!!inputLocationError || !inputLocation} + helperText={inputLocationError || undefined} + inputProps={{ + tooltip: { + title: "Input Location", + content: ( +
    + + Location of files to be indexed. + + + Examples based on integration type: + + + Cloud Storage: + +
      +
    • s3://bucketname/folder
    • +
    • gs://path
    • +
    • azureblob://path
    • +
    + + Git Repositories: + +
      +
    • https://github.com/owner/repo
    • +
    • https://gitlab.com/owner/repo
    • +
    + + Website Sitemap: + +
      +
    • + https://example.com/sitemap.xml (full path + required) +
    • +
    + + Single Page: + +
      +
    • https://example.com/page.html
    • +
    +
    + ), + }, + }} + /> +
    + + { + onChange(updateField(fileTypesPath, val, task)); + }} + path={fileTypesPath} + taskType={TaskType.LIST_FILES} + helperTextStyle={{ padding: 0 }} + fieldStyle={{ paddingX: 0 }} + > + { + // Validate and sanitize file types: remove dots and convert to lowercase + const sanitizedFileTypes = val.map((fileType) => + fileType.replace(/\./g, "").toLowerCase(), + ); + onChange( + updateField(fileTypesPath, sanitizedFileTypes, task), + ); + }} + value={fileTypes} + conductorInputProps={{ + tooltip: { + title: "Field Name", + content: + "List of file types to include (e.g., java, xls, csv, pdf, etc.). File types should be lowercase without dots.", + }, + }} + /> + + +
    +
    + + + + + onChange(updateField(outputLocationPath, changes, task)) + } + label="Output Location" + inputProps={{ + tooltip: { + title: "Output Location", + content: + "Location to store the output list of files as a text file.", + }, + }} + /> + + + +
    +
    + + + { + onChange(updateField(integrationNamesPath, val, task)); + }} + path={integrationNamesPath} + taskType={TaskType.LIST_FILES} + helperTextStyle={{ padding: 0 }} + fieldStyle={{ paddingX: 0 }} + > + <> + Map of integration types to integration names for multiple + integrations + + + + + onChange(updateField(integrationNamesPath, changes, task)) + } + value={integrationNames} + taskType={TaskType.LIST_FILES} + path={integrationNamesPath} + otherOptions={integrationNameOptions} + /> + + + + + + + + + +
    + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/MCPTaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/MCPTaskForm.tsx new file mode 100644 index 0000000000..1ef85cd82a --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/MCPTaskForm.tsx @@ -0,0 +1,467 @@ +import { + materialCells, + materialRenderers, +} from "@jsonforms/material-renderers"; +import { JsonForms } from "@jsonforms/react"; +import { + Box, + CircularProgress, + Grid, + ThemeProvider, + Typography, + createTheme, +} from "@mui/material"; +import { BookIcon, GearIcon } from "@phosphor-icons/react"; +import { ConductorAutoComplete } from "components/v1"; +import { IntegrationIcon } from "components/IntegrationIcon"; +import { useNavigate } from "react-router"; +import { colors } from "theme/tokens/variables"; +import { TaskType } from "types"; +import { updateField } from "utils/fieldHelpers"; +import { + useMCPIntegrations, + useMCPTools, +} from "utils/hooks/useMCPIntegrations"; +import { downgradeSchemaToDraft7 } from "utils/json"; +import { useFetch } from "utils/query"; +import { MaybeVariable } from "./MaybeVariable"; +import { Optional } from "./OptionalFieldForm"; +import TaskFormSection from "./TaskFormSection"; +import { TaskFormProps } from "./types"; +import { useMemo } from "react"; + +export const MCPTaskForm = ({ task, onChange }: TaskFormProps) => { + const navigate = useNavigate(); + const customTheme = createTheme({ + components: { + MuiTextField: { + styleOverrides: { + root: { + backgroundColor: "#FFFFFF", + fontSize: "14px", + fontWeight: 200, + minHeight: "unset", + marginBottom: "16px", + + // Remove autofill background's input + "& input:-webkit-autofill": { + WebkitBoxShadow: "0 0 0 100px #ffffff inset", + }, + + "& ::placeholder": { + color: "#AFAFAF", + }, + }, + }, + }, + MuiFormControl: { + styleOverrides: { + root: { + marginBottom: "16px", + }, + }, + }, + MuiInputLabel: { + styleOverrides: { + root: { + fontSize: "14px", + fontWeight: 200, + pointerEvents: "auto", + transform: "translate(12px, -9px) scale(0.857)", + color: "#494949", + + "&.Mui-focused": { + fontWeight: 500, + color: "#1976D2", + }, + + "&.Mui-error": { + color: "#D6423B", + }, + + "&.Mui-disabled": { + color: "#858585", + }, + }, + }, + }, + MuiOutlinedInput: { + styleOverrides: { + root: { + backgroundColor: "#FFFFFF", + fontSize: "14px", + fontWeight: 200, + color: "#060606", + minHeight: "unset", + + ".MuiInputBase-input": { + padding: "14px 8px 8px 8px", + "&.Mui-disabled": { + WebkitTextFillColor: "#494949", + }, + }, + + ".MuiOutlinedInput-notchedOutline": { + borderWidth: 1, + borderStyle: "solid", + borderRadius: "4px", + borderColor: "#AFAFAF", + + // This will make the legend has same size with the label + "& legend": { + maxWidth: "100%", + fontSize: "0.857em", + fontWeight: 200, + }, + }, + + "&:hover .MuiOutlinedInput-notchedOutline": { + borderColor: "#1876D1", + borderWidth: 1, + }, + + "&.Mui-focused .MuiOutlinedInput-notchedOutline": { + borderWidth: 1, + borderColor: "#1876D1", + + "& legend": { + fontWeight: 500, + }, + }, + + "&.Mui-focused": { + backgroundColor: "#FFFFFF", + }, + + "&.Mui-error": { + color: "#D6423B", + + ".MuiOutlinedInput-notchedOutline": { + borderColor: "#D6423B", + }, + }, + + "&.Mui-disabled": { + WebkitTextFillColor: "#494949", + borderColor: "#AFAFAF", + backgroundColor: "#ECECEC", + }, + + ".MuiInputBase-inputMultiline": { + p: 0, + }, + + "&.MuiInputBase-multiline": { + p: "14px 8px 8px 8px", + }, + }, + }, + }, + MuiFormHelperText: { + styleOverrides: { + root: { + fontSize: "0.857em", + color: "#494949", + paddingLeft: "8px", + marginTop: "4px", + marginLeft: "0px", + + "&.Mui-error": { + color: "#D6423B", + }, + + "&.Mui-disabled": { + color: "#060606", + }, + }, + }, + }, + MuiIconButton: { + styleOverrides: { + root: { + // Clear button visibility control + "&[aria-label='clear value']": { + visibility: "visible", + }, + }, + }, + }, + }, + }); + + const { integrations, isLoading: isLoadingIntegrations } = + useMCPIntegrations(); + const { tools, isLoading: isLoadingTools } = useMCPTools( + task?.inputParameters?.integrationName, + ); + + const hasValidIntegration = Boolean( + task?.inputParameters?.integrationName && + task?.inputParameters?.integrationName !== null, + ); + const hasValidMethod = Boolean( + task?.inputParameters?.method && task?.inputParameters?.method !== null, + ); + + const { data: toolData, isLoading: isToolDataLoading } = useFetch( + `/integrations/${task?.inputParameters?.integrationName}/def/api/${task?.inputParameters?.method}`, + { + enabled: hasValidIntegration && hasValidMethod, + }, + ); + + // Prepare and validate the schema for JsonForms + const processedSchema = useMemo(() => { + if (!toolData?.inputSchema?.data) return null; + + const schemaData = toolData.inputSchema.data; + if ( + typeof schemaData !== "object" || + Object.keys(schemaData).length === 0 + ) { + return null; + } + + // Check if schema has properties (actual fields to render) + if ( + !schemaData.properties || + Object.keys(schemaData.properties).length === 0 + ) { + return null; + } + + try { + return downgradeSchemaToDraft7(schemaData); + } catch (error) { + console.error("[MCPTaskForm] Error processing schema:", error); + return null; + } + }, [toolData?.inputSchema?.data]); + + return ( + + onChange(updateField("inputParameters", val, task))} + path={"inputParameters"} + taskType={TaskType.MCP} + > + + {isToolDataLoading ? ( + + + + + + ) : ( + + + + + + + + {task?.inputParameters?.method} + + + { + if (task?.inputParameters?.integrationName) { + navigate( + `/integrations/${encodeURIComponent( + task.inputParameters.integrationName, + )}/configuration`, + ); + } + }} + > + Configuration{" "} + + + + Docs{" "} + + + + + {/* + {toolData?.description} + */} + + + + + i.status === "active") + .map((i) => i.name)} + onChange={(__, val) => { + // Get the selected integration's type + const selectedIntegration = (integrations || []).find( + (i) => i.name === val, + ); + + // Only keep integration name and type in inputParameters + onChange({ + ...task, + inputParameters: { + integrationName: val, + integrationType: selectedIntegration?.type || null, + }, + }); + }} + value={task?.inputParameters?.integrationName} + autoFocus + required + disableClearable + getOptionLabel={(option) => option} + loading={isLoadingIntegrations} + /> + + + { + onChange({ + ...task, + inputParameters: { + integrationName: + task?.inputParameters?.integrationName, + integrationType: + task?.inputParameters?.integrationType, + method: val?.api, + }, + }); + }} + value={ + tools?.find( + (t: { api: string }) => + t.api === task?.inputParameters?.method, + ) || null + } + autoFocus + required + disableClearable + getOptionLabel={(option) => option?.api || ""} + loading={isLoadingTools} + disabled={!task?.inputParameters?.integrationName} + /> + + + + + + + {processedSchema && ( + + + + onChange(updateField("inputParameters", data, task)) + } + renderers={materialRenderers} + cells={materialCells} + /> + + + )} + + + + )} + + + + + + + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/MaybeVariable.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/MaybeVariable.tsx new file mode 100644 index 0000000000..f05ff6e163 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/MaybeVariable.tsx @@ -0,0 +1,106 @@ +import { useMemo, Fragment, FunctionComponent, ReactNode } from "react"; +import { Article as FormIcon } from "@phosphor-icons/react"; +import { Box, ToggleButton, Tooltip, Stack, SxProps } from "@mui/material"; +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import _isString from "lodash/isString"; +import { FormTaskType } from "types/TaskType"; +import _path from "lodash/fp/path"; +import { taskGeneratorMap } from "components/flow/nodes"; +import _isNil from "lodash/isNil"; +import HelperText from "components/HelperText"; + +interface MaybeVariableProps { + value: string | any; + onChange: (v: any) => void; + path: string; + taskType: FormTaskType; + children?: ReactNode; + helperTextStyle?: SxProps; + fieldStyle?: SxProps; +} + +type ValidTypes = "string" | "object"; + +export const MaybeVariable: FunctionComponent = ({ + children, + value, + onChange, + path, + taskType, + helperTextStyle = {}, + fieldStyle = {}, +}) => { + const valueType = useMemo( + (): ValidTypes => (_isString(value) ? "string" : "object"), + [value], + ); + + const handleChangeType = () => { + const generateTask = taskGeneratorMap[taskType]; + const newTask = generateTask({}); + const valueForObject = _path(path, newTask); + onChange(_isNil(valueForObject) ? {} : valueForObject); + }; + + const referenceKey = path.split(".").slice(-1).join(""); + return ( + + + {valueType === "string" && ( + + + Selecting form fields will show form with default value + + + + + + + + )} + + {valueType === "string" ? ( + + + + + + + + ) : ( + children + )} + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/OpsGenieTaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/OpsGenieTaskForm.tsx new file mode 100644 index 0000000000..9bafc5194c --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/OpsGenieTaskForm.tsx @@ -0,0 +1,181 @@ +import { Box, Grid } from "@mui/material"; +import { path as _path } from "lodash/fp"; + +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import { + ConductorFlatMapForm, + ConductorFlatMapFormBase, +} from "components/v1/FlatMapForm/ConductorFlatMapForm"; +import { TaskType } from "types"; +import { ConductorCacheOutput } from "./ConductorCacheOutputForm"; +import { ConductorValueInput } from "./ConductorValueInput"; +import { updateField } from "utils/fieldHelpers"; +import TaskFormSection from "./TaskFormSection"; +import { TaskFormProps } from "./types"; + +const DEFAULT_VALUES_FOR_ARRAY = { object: [] }; + +const aliasLocationPath = "inputParameters.alias"; +const descriptionPath = "inputParameters.description"; +const messagePath = "inputParameters.message"; +const priorityPath = "inputParameters.priority"; +const entityPath = "inputParameters.entity"; +const tokenPath = "inputParameters.token"; +const actionsPath = "inputParameters.actions"; +const tagsPath = "inputParameters.tags"; +const inputParametersPath = "inputParameters"; +const detailsPath = "inputParameters.details"; + +export const OpsGenieTaskForm = ({ task, onChange }: TaskFormProps) => { + const alias = _path(aliasLocationPath, task); + const description = _path(descriptionPath, task); + const message = _path(messagePath, task); + const priority = _path(priorityPath, task); + const entity = _path(entityPath, task); + const token = _path(tokenPath, task); + const actions = _path(actionsPath, task); + const tags = _path(tagsPath, task); + + return ( + + + + + + onChange(updateField(aliasLocationPath, changes, task)) + } + /> + + + + onChange(updateField(descriptionPath, changes, task)) + } + /> + + + + + + + + onChange(updateField(inputParametersPath, changes, task)) + } + /> + + + + + + + + onChange(updateField(detailsPath, changes, task)) + } + /> + + + + + + + + onChange(updateField(messagePath, changes, task)) + } + /> + + + { + onChange(updateField(actionsPath, changes, task)); + }} + defaultObjectValue={DEFAULT_VALUES_FOR_ARRAY} + /> + + + + + onChange(updateField(priorityPath, changes, task)) + } + /> + + + + onChange(updateField(entityPath, changes, task)) + } + /> + + + + + + + + onChange(updateField(tokenPath, changes, task)) + } + /> + + + { + onChange(updateField(tagsPath, changes, task)); + }} + defaultObjectValue={DEFAULT_VALUES_FOR_ARRAY} + /> + + + + + + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/OptionalFieldForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/OptionalFieldForm.tsx new file mode 100644 index 0000000000..0ee9864a9f --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/OptionalFieldForm.tsx @@ -0,0 +1,34 @@ +import React, { FunctionComponent } from "react"; +import { Box, Switch } from "@mui/material"; +import { Grid } from "@mui/system"; +interface OptionalProps { + onChange: (value: any) => void; + taskJson: any; +} + +export const Optional: FunctionComponent = ({ + onChange, + taskJson, +}) => { + const handleChange = (e: React.ChangeEvent) => { + onChange({ ...taskJson, optional: e.target.checked }); + }; + return ( + + + + + Make Task Optional + + + The workflow continues unaffected by the task's outcome, whether it + fails or remains incomplete. + + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/ParseDocumentTaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/ParseDocumentTaskForm.tsx new file mode 100644 index 0000000000..adaa87cac4 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/ParseDocumentTaskForm.tsx @@ -0,0 +1,578 @@ +import { + Alert, + AlertTitle, + Box, + Chip, + FormHelperText, + Grid, + Slider, + Stack, + Typography, +} from "@mui/material"; +import ConductorInput from "components/v1/ConductorInput"; +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import { useCallback, useState } from "react"; +import { updateField } from "utils/fieldHelpers"; +import { useGetIntegration } from "utils/hooks"; +import { Optional } from "./OptionalFieldForm"; +import TaskFormSection from "./TaskFormSection"; +import { TaskFormProps } from "./types"; + +// Media type options for PARSE_DOCUMENT with comprehensive document types +const PARSE_DOCUMENT_MEDIA_TYPES = [ + { value: "auto", label: "Auto-detect (Recommended)" }, + // Office Documents + { + value: + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + label: "Word Document (.docx)", + }, + { + value: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + label: "Excel Spreadsheet (.xlsx)", + }, + { + value: + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + label: "PowerPoint Presentation (.pptx)", + }, + { + value: "application/msword", + label: "Word Document (.doc)", + }, + { + value: "application/vnd.ms-excel", + label: "Excel Spreadsheet (.xls)", + }, + { + value: "application/vnd.ms-powerpoint", + label: "PowerPoint Presentation (.ppt)", + }, + // PDF + { + value: "application/pdf", + label: "PDF Document", + }, + // HTML + { + value: "text/html", + label: "HTML", + }, + // Images (with OCR) + { + value: "image/jpeg", + label: "JPEG Image (OCR)", + }, + { + value: "image/png", + label: "PNG Image (OCR)", + }, + { + value: "image/gif", + label: "GIF Image (OCR)", + }, + { + value: "image/bmp", + label: "BMP Image (OCR)", + }, + { + value: "image/tiff", + label: "TIFF Image (OCR)", + }, + // Zipped Documents + { + value: "application/zip", + label: "ZIP Archive (Auto-extract)", + }, + { + value: "application/x-zip-compressed", + label: "ZIP Compressed (Auto-extract)", + }, + // Text Formats + { + value: "text/plain", + label: "Plain Text", + }, + { + value: "text/markdown", + label: "Markdown", + }, +]; + +// Media type configuration types +type MediaTypeConfigItem = { + description: string; + chunkSizeRecommendation: string; + matchers?: readonly string[]; + matchType?: "includes" | "startsWith"; +}; + +// Media type configuration for descriptions and recommendations +const MEDIA_TYPE_CONFIG: Record = { + auto: { + description: + "Document type will be automatically detected based on content and file extension. All content will be converted to Markdown format optimized for LLM processing.", + chunkSizeRecommendation: + "Default: 0 (no chunking). Set to positive value to enable semantic chunking of parsed content", + }, + office: { + description: + "Office document will be parsed and converted to Markdown format, preserving document structure, formatting, tables, and hierarchies.", + chunkSizeRecommendation: + "Recommended: 1500-2000 characters for document content to maintain context", + matchers: [ + "openxmlformats", + "msword", + "ms-excel", + "ms-powerpoint", + ] as const, + }, + pdf: { + description: + "PDF will be parsed with text extraction, OCR for scanned documents, and converted to Markdown while preserving structure.", + chunkSizeRecommendation: + "Recommended: 1500-2000 characters for document content to maintain context", + matchers: ["application/pdf"] as const, + }, + html: { + description: + "HTML content will be parsed and converted to clean Markdown format, removing scripts and styling while preserving semantic structure.", + chunkSizeRecommendation: + "Default: 0 (no chunking). Set to positive value to enable semantic chunking of parsed content", + matchers: ["text/html"] as const, + }, + image: { + description: + "Image will be processed with OCR (Optical Character Recognition) to extract text content and convert to Markdown format.", + chunkSizeRecommendation: + "Recommended: 1000-1500 characters for OCR-extracted text", + matchers: ["image/"] as const, + matchType: "startsWith" as const, + }, + zip: { + description: + "ZIP archive will be automatically extracted and all supported documents inside will be parsed and converted to Markdown.", + chunkSizeRecommendation: + "Recommended: 1500-2000 characters per document in archive", + matchers: ["zip"] as const, + }, + text: { + description: + "Text content will be parsed and converted to Markdown format with appropriate formatting.", + chunkSizeRecommendation: + "Default: 0 (no chunking). Set to positive value to enable semantic chunking of parsed content", + matchers: ["text/"] as const, + matchType: "startsWith" as const, + }, + default: { + description: + "Content will be parsed and converted to Markdown format for optimal LLM processing.", + chunkSizeRecommendation: + "Default: 0 (no chunking). Set to positive value to enable semantic chunking of parsed content", + }, +}; + +// Helper function to find matching config +const findMediaTypeConfig = (mediaType: string) => { + if (!mediaType || mediaType === "auto") { + return MEDIA_TYPE_CONFIG.auto; + } + + // Check each config type + for (const config of Object.values(MEDIA_TYPE_CONFIG)) { + if (!config.matchers) continue; + + const matchType = config.matchType || "includes"; + const matches = config.matchers.some((matcher) => { + if (matchType === "startsWith") { + return mediaType.startsWith(matcher); + } + return mediaType.includes(matcher); + }); + + if (matches) return config; + } + + return MEDIA_TYPE_CONFIG.default; +}; + +// Get description of what happens based on media type +const getMediaTypeDescription = (mediaType: string): string => { + return findMediaTypeConfig(mediaType).description; +}; + +// Get chunk size recommendation based on media type +const getChunkSizeRecommendation = (mediaType: string): string => { + return findMediaTypeConfig(mediaType).chunkSizeRecommendation; +}; + +// Estimate chunk count +const estimateChunkCount = ( + chunkSize: number, + contentLength: number = 0, +): string => { + if (!chunkSize || chunkSize <= 0) + return "No chunking (entire document as one piece)"; + if (!contentLength) return "Depends on document size"; + return `Approximately ${Math.ceil(contentLength / chunkSize)} chunks`; +}; + +export const ParseDocumentTaskForm = ({ task, onChange }: TaskFormProps) => { + // Get current values from task + const integrationName = task.inputParameters?.integrationName || ""; + const url = task.inputParameters?.url || ""; + const mediaType = task.inputParameters?.mediaType || "auto"; + const chunkSize = task.inputParameters?.chunkSize || 0; + + // need to fetch compatible integration names and pass them to the integrationName autocomplete options + const integrations = useGetIntegration({}); + + // Filter integrations to only include git, aws, and gcp types + const integrationNameOptions = + integrations?.data + ?.filter((integration) => { + const type = integration?.type?.toLowerCase() || ""; + // Filter by type containing git, aws, + return type === "git" || type === "aws"; + }) + ?.map((integration) => integration?.name) || []; + + // Local state for URL validation + const [urlError, setUrlError] = useState(""); + + // Validate URL format + const validateUrl = useCallback((urlValue: string) => { + if (!urlValue) { + setUrlError(""); + return true; + } + + // Allow template variables and JSON path expressions + if (urlValue.includes("${") || urlValue.includes("$.")) { + setUrlError(""); + return true; + } + + // Basic URL validation + const urlPatterns = [ + /^s3:\/\/.+/i, + /^gs:\/\/.+/i, + /^https?:\/\/.+/i, + /^file:\/\/.+/i, + /^git:\/\/.+/i, + ]; + + const isValid = urlPatterns.some((pattern) => pattern.test(urlValue)); + if (!isValid) { + setUrlError( + "Invalid URL format. Must use s3://, gs://, https://, http://, file://, or git:// protocol", + ); + return false; + } + + setUrlError(""); + return true; + }, []); + + // Handlers + const handleIntegrationNameChange = useCallback( + (value: string) => { + onChange(updateField("inputParameters.integrationName", value, task)); + }, + [onChange, task], + ); + + const handleUrlChange = useCallback( + (value: string) => { + validateUrl(value); + onChange(updateField("inputParameters.url", value, task)); + }, + [onChange, task, validateUrl], + ); + + const handleMediaTypeChange = useCallback( + (value: string) => { + onChange(updateField("inputParameters.mediaType", value || "auto", task)); + }, + [onChange, task], + ); + + const handleChunkSizeChange = useCallback( + (value: number) => { + onChange(updateField("inputParameters.chunkSize", value, task)); + }, + [onChange, task], + ); + + // Slider marks for chunk size + const sliderMarks = [ + { value: 0, label: "0" }, + { value: 2500, label: "2.5K" }, + { value: 5000, label: "5K" }, + { value: 7500, label: "7.5K" }, + { value: 10000, label: "10K" }, + ]; + + return ( + + {/* Document Source Section */} + + + + + + + {integrationName && ( + + + Integration Selected: {integrationName} + Make sure your integration credentials are configured and + active. You can manage integrations in the Integrations page. + + + )} + + + + + + {integrationName && ( + + + + URL Format Examples: + + + {[ + "s3://bucket/document.pdf", + "https://example.com/document.pdf", + "file:///path/to/document.pdf", + ].map((example, idx) => ( + + • {example} + + ))} + + + + )} + + + + {/* Media Type Section */} + + + + opt.value)} + label="Media Type" + helperText="Select document type or use auto-detect. All documents will be converted to Markdown format." + getOptionLabel={(option) => + PARSE_DOCUMENT_MEDIA_TYPES.find((opt) => opt.value === option) + ?.label ?? option.toString() + } + renderOption={(props, option) => ( + + + {PARSE_DOCUMENT_MEDIA_TYPES.find( + (opt) => opt.value === option, + )?.label ?? option} + + + )} + /> + + + + + + Processing:{" "} + + + {getMediaTypeDescription(mediaType)} + + + + + + + + Supported Formats: + + + + + + + + + + + + + + + {/* Chunk Size Section */} + + + + + + { + const numValue = parseInt(value, 10); + if ( + !isNaN(numValue) && + numValue >= 0 && + numValue <= 10000 + ) { + handleChunkSizeChange(numValue); + } + }} + fullWidth + error={chunkSize < 0 || chunkSize > 10000} + helperText="Enter 0 for no chunking, or a value between 100 and 10000 for semantic chunking" + inputProps={{ min: 0, max: 10000 }} + /> + + + + + + Result:{" "} + + + {estimateChunkCount(chunkSize)} + + + {chunkSize === 0 + ? "The entire document will be returned as a single Markdown output" + : `Document will be split into semantic chunks of ~${chunkSize} characters`} + + + + + + + + + handleChunkSizeChange(value as number)} + min={0} + max={10000} + step={100} + marks={sliderMarks} + valueLabelDisplay="auto" + sx={{ mt: 2 }} + /> + + + + + + {getChunkSizeRecommendation(mediaType)} + + + + + + + Markdown Conversion:{" "} + + + All parsed content is converted to Markdown format, which is + optimized for LLM processing and maintains document structure, + headings, lists, tables, and formatting. + + + + + + + + + + + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/QueryProcessorTaskForm/MetricsTypeForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/QueryProcessorTaskForm/MetricsTypeForm.tsx new file mode 100644 index 0000000000..53d2cb60ef --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/QueryProcessorTaskForm/MetricsTypeForm.tsx @@ -0,0 +1,82 @@ +import { Box, Grid } from "@mui/material"; + +import { ConductorCodeBlockInput } from "components/v1/ConductorCodeBlockInput"; +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import { configurePromQl } from "utils/monacoUtils/CodeEditorUtils"; +import { useTaskForm } from "../hooks/useTaskForm"; +import { TaskFormProps } from "../types"; + +export const MetricsTypeForm = ({ task, onChange }: TaskFormProps) => { + const [query, setQuery] = useTaskForm("inputParameters.metricsQuery", { + task, + onChange, + }); + const [metricsStart, setMetricsStart] = useTaskForm( + "inputParameters.metricsStart", + { + task, + onChange, + }, + ); + const [metricsEnd, setMetricsEnd] = useTaskForm( + "inputParameters.metricsEnd", + { + task, + onChange, + }, + ); + const [metricsStep, setMetricsStep] = useTaskForm( + "inputParameters.metricsStep", + { + task, + onChange, + }, + ); + return ( + + + + + + + {"Start time from (Now - "} + + + + {`mins) to (Now -`} + + + + {"mins)"} + + + + + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/QueryProcessorTaskForm/QueryProcessorTaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/QueryProcessorTaskForm/QueryProcessorTaskForm.tsx new file mode 100644 index 0000000000..bbc3497a4c --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/QueryProcessorTaskForm/QueryProcessorTaskForm.tsx @@ -0,0 +1,247 @@ +import { Box, Grid } from "@mui/material"; +import RadioButtonGroup from "components/RadioButtonGroup"; +import ConductorInput from "components/v1/ConductorInput"; +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import { path as _path } from "lodash/fp"; +import { WorkflowExecutionStatus } from "types/Execution"; +import { QueryProcessorType } from "types/TaskType"; +import { updateField } from "utils/fieldHelpers"; +import { useWorkflowNames } from "utils/query"; +import { ConductorValueInput } from "../ConductorValueInput"; +import { useTaskForm } from "../hooks/useTaskForm"; +import TaskFormSection from "../TaskFormSection"; +import { TaskFormProps } from "../types"; +import { MetricsTypeForm } from "./MetricsTypeForm"; + +const DEFAULT_VALUES_FOR_ARRAY = { object: [] }; +const statusOptions = Object.values(WorkflowExecutionStatus); +const queryProcessorTypePath = "inputParameters.queryType"; +const workflowNamesPath = "inputParameters.workflowNames"; +const correlationIdsPath = "inputParameters.correlationIds"; +const statusesPath = "inputParameters.statuses"; +const freeTextPath = "inputParameters.freeText"; +const startTimeFromPath = "inputParameters.startTimeFrom"; +const startTimeToPath = "inputParameters.startTimeTo"; +const endTimeFromPath = "inputParameters.endTimeFrom"; +const endTimeToPath = "inputParameters.endTimeTo"; + +export const QueryProcessorTaskForm = ({ task, onChange }: TaskFormProps) => { + const [queryType, setQueryType] = useTaskForm(queryProcessorTypePath, { + task, + onChange, + }); + const workflowNames = _path(workflowNamesPath, task); + const correlationIds = _path(correlationIdsPath, task); + const statuses = _path(statusesPath, task); + const freeText = _path(freeTextPath, task); + const startTimeFrom = _path(startTimeFromPath, task); + const startTimeTo = _path(startTimeToPath, task); + const endTimeFrom = _path(endTimeFromPath, task); + const endTimeTo = _path(endTimeToPath, task); + + const workflowNamesOptions: string[] = useWorkflowNames(); + + const changeWorkflowNames = (value: string) => { + onChange(updateField(workflowNamesPath, value, task)); + }; + const changeCorrelationIds = (value: string) => { + onChange(updateField(correlationIdsPath, value, task)); + }; + const changeStatuses = (value: string | string[]) => { + onChange(updateField(statusesPath, value, task)); + }; + const changeFreeText = (value: string) => { + onChange(updateField(freeTextPath, value, task)); + }; + const changeStartTimeFrom = (value: any) => { + onChange(updateField(startTimeFromPath, value, task)); + }; + const changeStartTimeTo = (value: any) => { + onChange(updateField(startTimeToPath, value, task)); + }; + + const changeEndTimeFrom = (value: any) => { + onChange(updateField(endTimeFromPath, value, task)); + }; + const changeEndTimeTo = (value: any) => { + onChange(updateField(endTimeToPath, value, task)); + }; + + const changeTemplateType = (type: string) => { + setQueryType(type); + const conductorApiTemplate = { + workflowNames: [], + statuses: [], + correlationIds: [], + }; + const metricsTemplate = { + metricsQuery: "", + metricsStart: "", + metricsEnd: "", + metricsStep: "", + }; + if (type === QueryProcessorType.CONDUCTOR_API) { + onChange({ + ...task, + inputParameters: { + ...conductorApiTemplate, + queryType: type, + }, + }); + } else if (type === QueryProcessorType.METRICS) { + onChange({ + ...task, + inputParameters: { + ...metricsTemplate, + queryType: type, + }, + }); + } + }; + + return ( + + + + + { + changeTemplateType(value); + }} + /> + + + + + {queryType === QueryProcessorType.CONDUCTOR_API && ( + + + { + changeWorkflowNames(val); + }} + dropDownOptions={workflowNamesOptions} + defaultObjectValue={DEFAULT_VALUES_FOR_ARRAY} + /> + + + { + changeCorrelationIds(val); + }} + defaultObjectValue={DEFAULT_VALUES_FOR_ARRAY} + /> + + + { + changeStatuses(val); + }} + defaultObjectValue={DEFAULT_VALUES_FOR_ARRAY} + /> + + + + + + + + + + + + + + + + + + + + + + + + + + )} + {queryType === QueryProcessorType.METRICS && ( + + )} + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/QueryProcessorTaskForm/index.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/QueryProcessorTaskForm/index.ts new file mode 100644 index 0000000000..a15c8c2a69 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/QueryProcessorTaskForm/index.ts @@ -0,0 +1 @@ +export * from "./QueryProcessorTaskForm"; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/RateLimitConfigForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/RateLimitConfigForm.tsx new file mode 100644 index 0000000000..f3d0923406 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/RateLimitConfigForm.tsx @@ -0,0 +1,82 @@ +import { Box, Grid } from "@mui/material"; +import ConductorInput from "components/v1/ConductorInput"; +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import ConductorTooltip from "components/conductorTooltip/ConductorTooltip"; + +type RateLimitConfigValue = { + rateLimitKey: string; + concurrentExecLimit: number; +}; +interface RateLimitConfigFormProps { + onChange: (value: RateLimitConfigValue) => void; + value: RateLimitConfigValue; +} +export default function RateLimitConfigForm({ + onChange, + value, +}: RateLimitConfigFormProps) { + const handleRateLimitKeyChange = (val: string) => { + onChange({ ...value, rateLimitKey: val }); + }; + const handleConcurrentExecLimitChange = (val: number) => { + onChange({ ...value, concurrentExecLimit: val }); + }; + return ( + <> + + + Rate Limit + + + Limits the number of workflow executions at any given time. + + + + + + <>Rate limit key + + } + /> + + } + /> + + + + handleConcurrentExecLimitChange(parseInt(val)) + } + /> + + + + ); +} diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SchemaForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SchemaForm.tsx new file mode 100644 index 0000000000..8e0bfad419 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SchemaForm.tsx @@ -0,0 +1,316 @@ +import { Grid, Stack, Tooltip } from "@mui/material"; +import { NotePencilIcon as EditIcon, EyeIcon } from "@phosphor-icons/react"; +import MuiIconButton from "components/MuiIconButton"; +import { chain, map } from "lodash"; +import { ConductorNameVersionField } from "components/v1/ConductorNameVersionField"; +import { pluginRegistry } from "plugins/registry"; +import { + forwardRef, + FunctionComponent, + useCallback, + useImperativeHandle, + useMemo, + useRef, + useState, +} from "react"; +import { SchemaDefinition } from "types/SchemaDefinition"; +import { EnforceSchema } from "./EnforceSchemaForm"; +import TaskFormSection from "./TaskFormSection"; + +export interface SchemaFormValue { + name: string; + version?: number; + type?: string; +} + +interface SchemaFormItemProps { + onChange: (value?: SchemaFormValue) => void; + value?: SchemaFormValue; + label: string; + onRefetch: () => void; + disabled?: boolean; +} + +const SchemaFormItem = forwardRef< + { + refetch: () => void; + }, + SchemaFormItemProps +>(({ onChange, value, label, disabled, onRefetch }, ref) => { + const conductorNameVersionFieldRef = useRef<{ refetch: () => void }>(null); + const [editingSchema, setEditingSchema] = useState(false); + const [previewSchema, setPreviewSchema] = useState(false); + + // Get dialog components from plugin registry (enterprise-only) + const SchemaEditDialog = pluginRegistry.getSchemaEditDialog(); + const SchemaPreviewDialog = pluginRegistry.getSchemaPreviewDialog(); + + useImperativeHandle(ref, () => ({ + refetch: () => { + conductorNameVersionFieldRef.current?.refetch(); + }, + })); + + const openEditSchema = useCallback(() => { + setEditingSchema(true); + }, []); + + const handlePreviewSchema = () => setPreviewSchema(true); + const closePreviewSchema = () => setPreviewSchema(false); + + const closeEditSchema = useCallback( + ( + schema: + | { + name: string; + version?: number; + } + | undefined, + ) => { + if (schema) { + onChange({ ...schema, type: "JSON" }); + onRefetch(); + } + setEditingSchema(false); + }, + [onChange, onRefetch], + ); + + const handleNameVersionChange = ( + val: + | { + name?: string; + version?: number; + } + | undefined, + ) => { + if (val && val.name) { + onChange({ + name: val.name, + version: val.version, + type: "JSON", + }); + } else { + onChange(undefined); + } + }; + return ( + <> + + + + chain(data) + .groupBy("name") + .map((group, key) => ({ + name: key, + versions: map(group, "version"), + })) + .value() + } + value={value} + onChange={handleNameVersionChange} + /> + + {/* Preview button - only show if SchemaPreviewDialog is available */} + {SchemaPreviewDialog && ( + + + + + + )} + {/* Edit button - only show if SchemaEditDialog is available */} + {SchemaEditDialog && ( + + + + + + )} + + + {editingSchema && value && SchemaEditDialog && ( + + )} + {previewSchema && value && SchemaPreviewDialog && ( + + )} + + ); +}); + +export interface SchemaFormPropsValue { + inputSchema?: SchemaFormValue; + outputSchema?: SchemaFormValue; + enforceSchema?: boolean; +} + +export interface SchemaFormProps { + value?: SchemaFormPropsValue; + onChange: (value?: SchemaFormPropsValue) => void; + hideOutputSchema?: boolean; + hideInputSchema?: boolean; + hideEnforceSchema?: boolean; +} + +export const SchemaForm: FunctionComponent = ({ + onChange, + value, + hideInputSchema, + hideOutputSchema, + hideEnforceSchema, +}) => { + const inputSchemaRef = useRef<{ refetch: () => void }>(null); + const outputSchemaRef = useRef<{ refetch: () => void }>(null); + + const handleEnforceSchemaChange = useCallback( + ({ + inputSchema, + outputSchema, + }: { + inputSchema?: SchemaFormValue; + outputSchema?: SchemaFormValue; + }) => { + if (!inputSchema && !outputSchema) { + return false; + } else { + return true; + } + }, + [], + ); + + const handleEnforceSchemaSwitchChange = useCallback( + (checked: boolean) => { + onChange({ + ...value, + enforceSchema: checked, + }); + }, + [onChange, value], + ); + + const handleOnInputSchemaChange = useCallback( + (schema?: SchemaFormValue) => { + const enforceSchema = handleEnforceSchemaChange({ + inputSchema: schema, + outputSchema: value?.outputSchema, + }); + onChange({ + ...value, + inputSchema: schema, + enforceSchema, + }); + }, + [onChange, value, handleEnforceSchemaChange], + ); + const handleOnOutputSchemaChange = useCallback( + (schema?: SchemaFormValue) => { + const enforceSchema = handleEnforceSchemaChange({ + inputSchema: value?.inputSchema, + outputSchema: schema, + }); + onChange({ + ...value, + outputSchema: schema, + enforceSchema, + }); + }, + [onChange, value, handleEnforceSchemaChange], + ); + + const triggerRefetchOnBothSchemas = useCallback(() => { + if (inputSchemaRef.current) { + inputSchemaRef.current.refetch(); + } + if (outputSchemaRef.current) { + outputSchemaRef.current.refetch(); + } + }, []); + + const showEnforceSchemaSwitch = useMemo(() => { + return !!(value?.inputSchema || value?.outputSchema); + }, [value?.inputSchema, value?.outputSchema]); + + return ( + + + {!hideEnforceSchema && ( + + + + )} + {!hideInputSchema && ( + + + + )} + {!hideOutputSchema && ( + + + + )} + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SendgridForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SendgridForm.tsx new file mode 100644 index 0000000000..2512e3150d --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SendgridForm.tsx @@ -0,0 +1,154 @@ +import { Box, Grid } from "@mui/material"; +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import { UseQueryResult } from "react-query"; +import { IntegrationCategory, IntegrationDef } from "types"; +import { EMAIL_CONTENT_TYPE_SUGGESTIONS } from "utils/constants/emailContentTypeSuggestions"; +import { useIntegrationProviders } from "utils/useIntegrationProviders"; +import { ConductorCacheOutput } from "./ConductorCacheOutputForm"; +import { Optional } from "./OptionalFieldForm"; +import TaskFormSection from "./TaskFormSection"; +import { TaskFormProps } from "./types"; +import { useGetSetHandler } from "./useGetSetHandler"; + +const FROM = "inputParameters.from"; +const TO = "inputParameters.to"; +const SUBJECT = "inputParameters.subject"; +const CONTENT_TYPE = "inputParameters.contentType"; +const CONTENT = "inputParameters.content"; +const SENDGRID_CONFIGURATION = "inputParameters.sendgridConfiguration"; + +export const SendgridForm = (props: TaskFormProps) => { + const { task, onChange } = props; + const [from, handlerFrom] = useGetSetHandler(props, FROM); + + const [to, handlerTo] = useGetSetHandler(props, TO); + + const [subject, handlerSubject] = useGetSetHandler(props, SUBJECT); + + const [contentType, handlerContentType] = useGetSetHandler( + props, + CONTENT_TYPE, + ); + + const [content, handlerContent] = useGetSetHandler(props, CONTENT); + + const [sendgridConfiguration, handlerSendgridConfiguration] = + useGetSetHandler(props, SENDGRID_CONFIGURATION); + + const { data: sendgridIntegrations }: UseQueryResult = + useIntegrationProviders({ + category: IntegrationCategory.EMAIL, // May need better filters if we add more email type + activeOnly: false, + }); + + return ( + + + + + + + + + + +
    + + + + + +
    + + + + + +
    + + + + + +
    + + + item.name) || [] + } + label="SendGrid Configuration" + /> + + +
    + + + + + + + +
    + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/ServiceRegistrySelector.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/ServiceRegistrySelector.tsx new file mode 100644 index 0000000000..a1dd1521c6 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/ServiceRegistrySelector.tsx @@ -0,0 +1,427 @@ +import { + Box, + Button, + CircularProgress, + Grid, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, +} from "@mui/material"; +import { MagicWand } from "@phosphor-icons/react"; +import MuiButton from "components/MuiButton"; +import UIModal from "components/UIModal"; +import { ConductorAutoComplete } from "components/v1"; +import ConductorInput from "components/v1/ConductorInput"; +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import { MessageContext } from "components/v1/layout/MessageContext"; +import _every from "lodash/every"; +import _isEmpty from "lodash/isEmpty"; +import _isNil from "lodash/isNil"; +import { replaceDynamicParams } from "utils/remoteServices"; +import { Method, ServiceDefDto } from "types/RemoteServiceTypes"; +import { useContext, useMemo, useState } from "react"; +import { ActorRef } from "xstate"; +import { useServiceMethodsDefinition } from "./HTTPTaskForm/state/hook"; +import { ServiceMethodsMachineEvents } from "./HTTPTaskForm/state/types"; + +const TruncatedText = ({ + text, + maxLines = 3, +}: { + text?: string; + maxLines?: number; +}) => { + const [isExpanded, setIsExpanded] = useState(false); + + return ( + setIsExpanded(!isExpanded)} + > + {text ?? ""} + + ); +}; + +interface ServiceRegistryPopulatorProps { + modalShow: boolean; + setModalShow: (val: boolean) => void; + handleSelectService: (val: string) => void; + selectedService: ServiceDefDto; + services: ServiceDefDto[]; + handleSelectMethod: (val: string) => void; + selectedMethod: Method; + selectedServiceMethodsOptions: Method[]; + serviceType: string; + actor: ActorRef; + handleSelectHost?: (val: string) => void; + selectedHost?: string; +} +function ServiceRegistryPopulator({ + modalShow, + setModalShow, + handleSelectService, + handleSelectHost, + selectedService, + services, + handleSelectMethod, + selectedMethod, + selectedServiceMethodsOptions, + serviceType, + actor, + selectedHost, +}: ServiceRegistryPopulatorProps) { + const { setMessage } = useContext(MessageContext); + const [requestParams, setRequestParams] = useState>({}); + const [{ isInIdleState }, { handleUpdateTemplate }] = + useServiceMethodsDefinition(actor); + + const isParamsValid = useMemo(() => { + if (serviceType !== "HTTP") { + return true; + } + if ( + !selectedMethod?.requestParams || + _isEmpty(selectedMethod.requestParams) + ) { + return true; + } + + return _every(selectedMethod.requestParams, (param, _key) => { + if (!param?.required) return true; + const value = requestParams?.[param.name]?.value; + return !_isNil(value) && value !== ""; + }); + }, [selectedMethod?.requestParams, requestParams, serviceType]); + + const handleExecute = () => { + if (selectedMethod?.methodType && selectedService?.serviceURI) { + const { url: updatedUrl, headers } = replaceDynamicParams( + selectedMethod?.methodName, + requestParams, + ); + const updatedHeaders = { + ...headers, + ...(selectedService?.authMetadata && + selectedService?.authMetadata?.key && + selectedService?.authMetadata?.value && { + [selectedService?.authMetadata?.key]: + selectedService?.authMetadata?.value, + }), + }; + if (selectedService?.type === "gRPC") { + handleUpdateTemplate({ + updatedUrl: selectedHost ?? "", + headers: updatedHeaders, + }); + } else { + handleUpdateTemplate({ updatedUrl, headers: updatedHeaders }); + } + setModalShow(false); + setMessage({ + severity: "success", + text: `Applied successfully`, + }); + } + }; + + const handleInputChange = ( + data: { name: string; type: string; required: boolean }, + value: string, + ) => { + const updatedRequestParams = { + ...requestParams, + [data.name]: { ...data, value: value }, + }; + setRequestParams(updatedRequestParams); + }; + + return ( + + } + variant="text" + size="small" + onClick={() => setModalShow(true)} + > + Populate from remote services + + } + > + + + { + handleSelectService(val); + setRequestParams({}); + }} + value={selectedService?.name ?? ""} + options={ + services + ?.filter((item: ServiceDefDto) => item.type === serviceType) + ?.map((item: ServiceDefDto) => item?.name) ?? [] + } + label="Service" + /> + + + { + handleSelectHost?.(val); + }} + value={selectedHost ?? ""} + options={[ + ...(selectedService?.servers?.map((server) => server.url) ?? + []), + ...(selectedService?.serviceURI + ? [selectedService?.serviceURI] + : []), + ]} + label={serviceType === "gRPC" ? "Host:Port" : "Host"} + /> + + + { + handleSelectMethod(val); + setRequestParams({}); + }} + value={ + selectedMethod + ? `[${selectedMethod?.methodType}]` + + selectedMethod?.methodName + : "" + } + options={selectedServiceMethodsOptions ?? []} + renderOption={(props, option) => ( + + {option} + + )} + label="Service method" + /> + + {serviceType === "HTTP" && ( + + + + )} + {selectedMethod?.description && ( + + + + + ⓘ + + + + + + )} + {selectedMethod?.deprecated && ( + + + + ⚠️ This method is deprecated and may be removed in future + versions. + + + + )} + {serviceType === "HTTP" && ( + + + + + + Name + Description + + + + {selectedMethod?.requestParams && + selectedMethod?.requestParams?.length > 0 ? ( + selectedMethod?.requestParams?.map((row) => ( + + + + + {row.name} + {row.required && ( + + * required + + )} + + + {row?.schema?.type} + + + ({row.type}) + + + + + handleInputChange(row, val)} + /> + + + )) + ) : ( + No parameters + )} + +
    +
    +
    + )} +
    + + + +
    +
    + ); +} + +export default ServiceRegistryPopulator; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SetVariableOperatorForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SetVariableOperatorForm.tsx new file mode 100644 index 0000000000..e2f8e2e1a2 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SetVariableOperatorForm.tsx @@ -0,0 +1,40 @@ +import { Box, Grid } from "@mui/material"; +import { ConductorFlatMapFormBase } from "components/v1/FlatMapForm/ConductorFlatMapForm"; +import { path as _path } from "lodash/fp"; +import { updateField } from "utils/fieldHelpers"; +import { Optional } from "./OptionalFieldForm"; +import TaskFormSection from "./TaskFormSection"; +import { TaskFormProps } from "./types"; + +const inputParametersPath = "inputParameters"; + +export const SetVariableOperatorForm = ({ task, onChange }: TaskFormProps) => { + return ( + + + + + + onChange(updateField(inputParametersPath, value, task)) + } + /> + + + + + + + + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SimpleTaskForm/SampleCode.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SimpleTaskForm/SampleCode.ts new file mode 100644 index 0000000000..d56b3cd0d2 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SimpleTaskForm/SampleCode.ts @@ -0,0 +1,494 @@ +const formatInputParams = (inputParamKeys: string[]): string => { + if (!inputParamKeys?.length) return ""; + return inputParamKeys + .map((key) => `@InputParam("${key}") Object ${key}`) + .join(", "); +}; +export const sampleJavaCode = ({ + taskDefName, + inputParamKeys, +}: { + taskDefName: string; + inputParamKeys: string[]; +}) => + `/* + * To set up the project, install the dependencies, and run the application, use the following commands: + * + * 1. Create a new Maven project or navigate to your existing project. + * + * Project Directory Structure: + * + * conductor-sample/ + * ├── src/ + * │ └── main/ + * │ └── java/ + * │ └── org/ + * │ └── example/ + * │ └── Main.java (This is your main Java file) + * + * + * 2. Add the following dependency to your pom.xml file: + * + * + * io.orkes.conductor + * orkes-conductor-client + * 1.1.14 + * + * + * 3. Run the following command to download and install the dependencies: + * mvn install + * + * 4. To compile and run the project, use the following command: + * mvn exec:java -Dexec.mainClass="com.example.Main" + * + */ + +package org.example; +import com.netflix.conductor.sdk.workflow.executor.WorkflowExecutor; +import com.netflix.conductor.sdk.workflow.task.InputParam; +import com.netflix.conductor.sdk.workflow.task.WorkerTask; +import io.orkes.conductor.client.ApiClient; +import io.orkes.conductor.client.OrkesClients; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Map; +import java.util.Collection; + +public class Main { + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private static String convertToString(Object value) { + if (value == null) return "null"; + try { + return (value instanceof Collection || value.getClass().isArray() || value instanceof Map) + ? objectMapper.writeValueAsString(value) + : String.valueOf(value); + } catch (Exception e) { + return String.valueOf(value); + } + } + + public static void main(String[] args) + { + System.out.println("Hello world!"); + ApiClient apiClient = new ApiClient("${ + window.location.origin + }/api","some-key","some-secret"); + OrkesClients oc = new OrkesClients(apiClient); + + WorkflowExecutor executor = new WorkflowExecutor( + oc.getTaskClient(), + oc.getWorkflowClient(), + oc.getMetadataClient(), + 10); + + executor.initWorkers("org.example"); + } + + @WorkerTask("${taskDefName}") + public String greet(${formatInputParams(inputParamKeys)}) { + ${ + inputParamKeys && inputParamKeys?.length > 0 + ? `return String.format("Hello ${inputParamKeys + ?.map((_item) => `%s`) + .join(", ")}!", ${inputParamKeys + ?.map((item) => `convertToString(${item})`) + .join(", ")});` + : `return "Hello";` + } + } +} +`; + +export const samplePythonCode = ({ + taskDefName, + inputParamKeys, +}: { + taskDefName: string; + inputParamKeys?: string[]; +}) => `# To set up the project, install the dependencies, and run the application, follow these steps: +# +# 1. Create a Conda environment with the latest version of Python: +# conda create --name myenv python +# +# 2. Activate the environment: +# conda activate myenv +# +# 3. Install the necessary dependencies: +# pip install conductor-python +# +# 4. Run the Python script (replace script.py with your actual script name): +# python script.py + +from conductor.client.automator.task_handler import TaskHandler +from conductor.client.configuration.configuration import Configuration +from conductor.client.worker.worker_task import worker_task +import os + +os.environ['CONDUCTOR_SERVER_URL'] = '${window.location.origin}/api' +os.environ['CONDUCTOR_AUTH_KEY'] = 'SomeKey' +os.environ['CONDUCTOR_AUTH_SECRET'] = 'SomeValue' + +@worker_task(task_definition_name='${taskDefName}') +def greet(${ + inputParamKeys && inputParamKeys?.length > 0 + ? `${inputParamKeys?.map((item: string) => `${item}: str`)}` + : `` +}): + return f'Hello ${ + inputParamKeys && inputParamKeys?.length > 0 + ? inputParamKeys?.map((item: string) => `{${item}}`) + : `there!` + }' + +api_config = Configuration() + +task_handler = TaskHandler(configuration=api_config) +task_handler.start_processes() # starts polling for work +# task_handler.stop_processes() # stops polling for work`; + +export const sampleGolangCode = ({ + taskDefName, + inputParamKeys, +}: { + taskDefName: string; + inputParamKeys?: string[]; +}) => + `/* + * To set up the project, install the dependencies, and run the application, follow these steps: + * + * 1. Create a Go module for your project: + * go mod init mymodule + * + * 2. Install the Conductor Go SDK: + * go get github.com/conductor-sdk/conductor-go + * + * 3. Run the Go program (replace main.go with your actual file name): + * go run main.go + */ + +package main + +import ( + "fmt" + "os" + "time" + "encoding/json" + "strings" + + log "github.com/sirupsen/logrus" + + "github.com/conductor-sdk/conductor-go/sdk/client" + "github.com/conductor-sdk/conductor-go/sdk/model" + "github.com/conductor-sdk/conductor-go/sdk/settings" + + "github.com/conductor-sdk/conductor-go/sdk/worker" + "github.com/conductor-sdk/conductor-go/sdk/workflow/executor" +) + +var ( + apiClient = client.NewAPIClient( + authSettings(), + httpSettings(), + ) + taskRunner = worker.NewTaskRunnerWithApiClient(apiClient) + workflowExecutor = executor.NewWorkflowExecutor(apiClient) +) + +func authSettings() *settings.AuthenticationSettings { + key := os.Getenv("KEY") + secret := os.Getenv("SECRET") + // get your key and secret by generating an application + if key != "" && secret != "" { + return settings.NewAuthenticationSettings( + key, + secret, + ) + } + + return nil +} + +func httpSettings() *settings.HttpSettings { + url := "${window.location.origin}/api" + if url == "" { + log.Error("Error: CONDUCTOR_SERVER_URL env variable is not set") + os.Exit(1) + } + + return settings.NewHttpSettings(url) +} + // Helper function to convert input parameter value to string +func convertToString(value interface{}) string { + if value == nil { + return "null" + } + switch v := value.(type) { + case []interface{}, map[string]interface{}: + jsonBytes, err := json.Marshal(v) + if err != nil { + return fmt.Sprintf("%v", v) + } + return string(jsonBytes) + default: + return fmt.Sprintf("%v", v) + } +} + +func Greet(task *model.Task) (interface{}, error) { + var greetings strings.Builder + greetings.WriteString("Hello") + ${ + inputParamKeys && inputParamKeys.length > 0 + ? ` + // Convert and append each input parameter + ${inputParamKeys + .map( + (item) => `if val, ok := task.InputData["${item}"]; ok { + greetings.WriteString(", " + convertToString(val)) + }`, + ) + .join("\n ")}` + : "" + } + return map[string]interface{}{ + "greetings": greetings.String(), + }, nil +} + +func main() { + taskRunner.StartWorker("${taskDefName}", Greet, 1, time.Millisecond*100) + taskRunner.WaitWorkers(); +} +`; + +export const sampleCSharpCode = ({ + taskDefName, + inputParamKeys, +}: { + taskDefName: string; + inputParamKeys?: string[]; +}) => `/* + * To set up the project, install the dependencies, and run the application, follow these steps: + * + * 1. Create a new .NET project: + * dotnet new console -n MyProject + * + * 2. Change to the project directory: + * cd MyProject + * + * 3. Add the Conductor C# SDK: + * dotnet add package conductor-csharp + * + * 4. Add your worker code in Program.cs or create a separate class file for better organization. + * + * 5. Run the application: + * dotnet run + */ + +using Conductor.Api; +using Conductor.Client.Extensions; +using Conductor.Definition; +using Conductor.Client.Worker; +using Conductor.Client; +using Conductor.Client.Models; +using Conductor.Client.Interfaces; +using Task = Conductor.Client.Models.Task; +using System.Text.Json; +using Conductor.Executor; +using Conductor.Client.Authentication; +using Conductor.Definition.TaskType; +using System; +using System.Threading; +using System.Threading.Tasks; +var configuration = new Configuration() +{ + BasePath = "${window.location.origin}/api", + AuthenticationSettings = new OrkesAuthenticationSettings("XXX", "XXXX") +}; +var host = WorkflowTaskHost.CreateWorkerHost(configuration, Microsoft.Extensions.Logging.LogLevel.Information, new SimpleWorker()); +await host.StartAsync(); +Thread.Sleep(TimeSpan.FromSeconds(100)); +public class SimpleWorker: IWorkflowTask +{ + public string TaskType + { + get; + } + public WorkflowTaskExecutorConfiguration WorkerSettings + { + get; + } + public SimpleWorker(string taskType = "${taskDefName}") + { + TaskType = taskType; + WorkerSettings = new WorkflowTaskExecutorConfiguration(); + } + public async System.Threading.Tasks.Task < TaskResult > Execute(Task task, CancellationToken token = + default) + { + return await System.Threading.Tasks.Task.Run(() => + { + var result = task.Completed(); + string outputString = "Hello world"; + result.OutputData = new Dictionary < string, object > + { + { + "result", + outputString ${ + inputParamKeys && inputParamKeys?.length > 0 + ? `+= " " + string.Join(" ", [${inputParamKeys.map( + (item) => `task.InputData["${item}"]`, + )}])` + : "" + } + } + }; + return result; + }); + } + public TaskResult Execute(Task task) + { + throw new NotImplementedException(); + } +}`; + +export const sampleJavaScriptCode = ({ + taskDefName, + accessToken, + inputParamKeys, +}: { + taskDefName: string; + accessToken: string; + inputParamKeys?: string[]; +}) => `/* + * To set up the project, install the dependencies, and run the application, follow these steps: + * + * 1. Install the Conductor JavaScript SDK: + * npm install @io-orkes/conductor-javascript + * or + * yarn add @io-orkes/conductor-javascript + * + * 2. Run the JavaScript file (replace yourFile.js with your actual file name): + * node yourFile.js + */ + +import { + orkesConductorClient, + TaskManager, +} from "@io-orkes/conductor-javascript"; + +async function test() { + const clientPromise = orkesConductorClient({ + // keyId: "XXX", // optional + // keySecret: "XXXX", // optional + TOKEN: "${accessToken}", + serverUrl: "${window.location.origin}/api" + }); + + const client = await clientPromise; + + const customWorker = { + taskDefName: "${taskDefName}", + execute: async ({ inputData${ + inputParamKeys && inputParamKeys?.length > 0 + ? `:{ ${inputParamKeys} }` + : `` + }, taskId }) => { + return { + outputData: { + greeting: \`Hello World ${inputParamKeys?.map( + (item) => `\${${item}}`, + )}\`, + }, + status: "COMPLETED", + }; + }, + }; + + const manager = new TaskManager(client, [customWorker], { + options: { pollInterval: 100, concurrency: 1 }, + }); + + manager.startPolling(); +} +test();`; + +export const sampleTypeScriptCode = ({ + taskDefName, + accessToken, + inputParamKeys, +}: { + taskDefName: string; + accessToken: string; + inputParamKeys?: string[]; +}) => `/* + * To set up the project, install the dependencies, and run the application, follow these steps: + * + * 1. Install the Conductor JavaScript SDK: + * npm install @io-orkes/conductor-javascript + * or + * yarn add @io-orkes/conductor-javascript + * + * 2. Install ts-node if not already installed: + * npm install ts-node + * or + * yarn add ts-node + * + * 3. Run the TypeScript file directly with ts-node (replace yourFile.ts with your actual file name): + * npx ts-node yourFile.ts + */ + +import { + ConductorWorker, + orkesConductorClient, + TaskManager, +} from "@io-orkes/conductor-javascript"; + +async function test() { + const clientPromise = orkesConductorClient({ + // keyId: "XXX", // optional + // keySecret: "XXXX", // optional + TOKEN: "${accessToken}", + serverUrl: "${window.location.origin}/api" + }); + + const client = await clientPromise; + + const customWorker: ConductorWorker = { + taskDefName: "${taskDefName}", + execute: async ({ inputData${ + inputParamKeys && inputParamKeys?.length > 0 + ? `:{ ${inputParamKeys} }` + : `` + }, taskId }) => { + return { + outputData: { + greeting: \`Hello World ${inputParamKeys?.map( + (item) => `\${${item}}`, + )}\`, + }, + status: "COMPLETED", + }; + }, + }; + + const manager = new TaskManager(client, [customWorker], { + options: { pollInterval: 100, concurrency: 1 }, + }); + + manager.startPolling(); +} +test();`; + +export const sampleClojureCode = `(defn create-tasks + "Returns workflow tasks" + [] + (vector (sdk/simple-task (:get-user-info constants) (:get-user-info constants) {:userId "\${workflow.input.userId}"}) + (sdk/switch-task "emailorsms" "\${workflow.input.notificationPref}" {"email" [(sdk/simple-task (:send-email constants) (:send-email constants) {"email" "\${get_user_info.output.email}"})] + "sms" [(sdk/simple-task (:send-sms constants) (:send-sms constants) {"phoneNumber" "\${get_user_info.output.phoneNumber}"})]} []))) + +(defn create-workflow + "Returns a workflow with tasks" + [tasks] + (merge (sdk/workflow (:workflow-name constants) tasks) {:inputParameters ["userId" "notificationPref"]})) +`; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SimpleTaskForm/SimpleTaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SimpleTaskForm/SimpleTaskForm.tsx new file mode 100644 index 0000000000..42c74633f6 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SimpleTaskForm/SimpleTaskForm.tsx @@ -0,0 +1,293 @@ +import { Editor } from "@monaco-editor/react"; +import { Box, Grid, IconButton, Link, Stack } from "@mui/material"; +import { ArrowsOutSimple, ArrowSquareOut } from "@phosphor-icons/react"; +import { Paper, Tab, Tabs } from "components"; +import MuiTypography from "components/MuiTypography"; +import { SnackbarMessage } from "components/SnackbarMessage"; +import UIModal from "components/UIModal"; +import { ConductorFlatMapFormBase } from "components/v1/FlatMapForm/ConductorFlatMapForm"; +import CopyIcon from "components/v1/icons/CopyIcon"; +import { path as _path } from "lodash/fp"; +import { useMemo, useState } from "react"; +import { getAccessToken } from "shared/auth/tokenManagerJotai"; +import { defaultEditorOptions, type EditorOptions } from "shared/editor"; +import { updateField } from "utils/fieldHelpers"; +import { ConductorCacheOutput } from "../ConductorCacheOutputForm"; +import { Optional } from "../OptionalFieldForm"; +import { SchemaForm } from "../SchemaForm"; +import TaskFormSection from "../TaskFormSection"; +import { TaskFormProps } from "../types"; +import { useSchemaFormHandler } from "../hooks/useSchemaFormHandler"; +import { + sampleClojureCode, + sampleCSharpCode, + sampleGolangCode, + sampleJavaCode, + sampleJavaScriptCode, + samplePythonCode, + sampleTypeScriptCode, +} from "./SampleCode"; +import { TemplateKeys } from "./TemplateKeys"; + +const inputParametersPath = "inputParameters"; +const SAMPLE_CODE_TABS = [ + "Java", + "Python", + "Golang", + "CSharp", + "JavaScript", + "TypeScript", + // "Clojure", +]; +const editorOption: EditorOptions = { + ...defaultEditorOptions, + tabSize: 2, + minimap: { enabled: false }, + quickSuggestions: true, + overviewRulerLanes: 0, + scrollbar: { + vertical: "hidden", + // this property is added because it was not allowing us to scroll when mouse pointer is over this component + alwaysConsumeMouseWheel: false, + }, + formatOnType: true, + readOnly: true, + wordWrap: "on", + scrollBeyondLastLine: false, + automaticLayout: true, +}; + +const SampleCodeSection = ({ + height = "300px", + selectedSample, + setSelectedSample, + displaySampleCode, + editorLanguage, + handleCopy, +}: { + height?: string; + selectedSample: string; + setSelectedSample: (sample: string) => void; + displaySampleCode: string; + editorLanguage: string; + handleCopy: () => void; +}) => { + return ( + <> + setSelectedSample(val)} + > + {SAMPLE_CODE_TABS.map((item) => ( + + ))} + + + + + + + + + + + + + ); +}; + +export const SimpleTaskForm = ({ task, onChange }: TaskFormProps) => { + const [selectedSample, setSelectedSample] = useState("Python"); + const [showAlert, setShowAlert] = useState(false); + const [showSampleModal, setShowSampleModal] = useState(false); + + const displaySampleCode = useMemo(() => { + const accessToken = getAccessToken() || ""; + const inputParamKeys = task?.inputParameters + ? Object.keys(task?.inputParameters) + : []; + if (selectedSample === "Java") { + return sampleJavaCode({ + taskDefName: task?.name ?? "greet", + inputParamKeys: inputParamKeys, + }); + } + if (selectedSample === "Python") { + return samplePythonCode({ + taskDefName: task?.name ?? "greet", + inputParamKeys: inputParamKeys, + }); + } + if (selectedSample === "Golang") { + return sampleGolangCode({ + taskDefName: task?.name ?? "greet", + inputParamKeys: inputParamKeys, + }); + } + if (selectedSample === "CSharp") { + return sampleCSharpCode({ + taskDefName: task?.name ?? "greet", + inputParamKeys: inputParamKeys, + }); + } + if (selectedSample === "JavaScript") { + return sampleJavaScriptCode({ + taskDefName: task?.name ?? "task_definition_name", + accessToken, + inputParamKeys: inputParamKeys, + }); + } + if (selectedSample === "TypeScript") { + return sampleTypeScriptCode({ + taskDefName: task?.name ?? "task_definition_name", + accessToken, + inputParamKeys: inputParamKeys, + }); + } + if (selectedSample === "Clojure") { + return sampleClojureCode; + } + return ""; + }, [selectedSample, task?.inputParameters, task?.name]); + + const handleCopy = () => { + setShowAlert(true); + const selectedCode = displaySampleCode; + navigator.clipboard.writeText(selectedCode ?? ""); + }; + + const editorLanguage = useMemo(() => { + if (selectedSample === "Golang") { + return "go"; + } + return selectedSample.toLowerCase(); + }, [selectedSample]); + + const handleSchemaChange = useSchemaFormHandler({ task, onChange }); + + return ( + + {showAlert && ( + setShowAlert(false)} + anchorOrigin={{ horizontal: "right", vertical: "bottom" }} + /> + )} + + + + + + onChange(updateField(inputParametersPath, value, task)) + } + /> + + + + + + + + + + + + ) => + onChange({ ...task, inputParameters: inputParams }) + } + /> + + + + Sample worker code + + + + + Generate your auth key and secret from{" "} + + + Applications + + + setShowSampleModal(true)} + style={{ marginLeft: "auto" }} + > + + + + + + } + description="All sample worker codes in one place" + enableCloseButton + > + + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SimpleTaskForm/TemplateKeys.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SimpleTaskForm/TemplateKeys.tsx new file mode 100644 index 0000000000..ade6c6d1e0 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SimpleTaskForm/TemplateKeys.tsx @@ -0,0 +1,176 @@ +import { Box, CircularProgress, Stack } from "@mui/material"; +import { Intersect } from "@phosphor-icons/react"; +import Button from "components/MuiButton"; +import IconButton from "components/MuiIconButton"; +import MuiTypography from "components/MuiTypography"; +import StrikedText from "components/StrikedText"; +import Text from "components/Text"; +import { FunctionComponent, useCallback, useMemo } from "react"; +import { Link } from "@mui/material"; +import { TaskDef } from "types"; +import { FIELD_TYPE_OBJECT, IObject } from "types/common"; +import { FEATURES, featureFlags, inferType, useFetch } from "utils"; +import TaskFormSection from "../TaskFormSection"; + +const taskVisibility = featureFlags.getValue(FEATURES.TASK_VISIBILITY, "READ"); + +const grayedTextFieldLikeStyles = { + padding: "4px 10px", + borderRadius: "5px", + background: "rgba(0,0,0,.15)", + width: "100%", +}; + +const deserializeOrDash = (value: any): string => { + try { + const result = JSON.stringify(value, null, 2); + return result; + } catch { + return "-"; + } +}; + +export interface TemplateKeysProps { + task: Partial; + onUniteParameter: (partialInputParams: Record) => void; +} + +export const TemplateKeys: FunctionComponent = ({ + task, + onUniteParameter, +}) => { + const { + data, + isFetching, + refetch: refetchAllDefinitions, + } = useFetch(`/metadata/taskdefs?access=${taskVisibility}`); + + const maybeTemplate = useMemo(() => { + const maybeSelectedTask = data?.find((t: TaskDef) => t.name === task.name); + return maybeSelectedTask?.inputTemplate; + }, [task, data]); + + const [currentTaskInputParams, inputParamsKeys] = useMemo(() => { + const inputParameters = task.inputParameters || {}; + return [inputParameters, Object.keys(inputParameters)]; + }, [task]); + + const handleUniteParameter = useCallback( + (keyParm: string) => { + onUniteParameter({ + ...currentTaskInputParams, + [keyParm]: maybeTemplate?.[keyParm], + }); + }, + [maybeTemplate, onUniteParameter, currentTaskInputParams], + ); + + const copyAllValues = useCallback(() => { + onUniteParameter({ + ...currentTaskInputParams, + ...maybeTemplate, + }); + }, [maybeTemplate, onUniteParameter, currentTaskInputParams]); + + const withTemplateKeysContent = useMemo( + () => + maybeTemplate == null ? ( + No default input templates configured + ) : ( + + + + + {Object.entries(maybeTemplate || {}).map(([key, value]) => { + const nonActionable = inputParamsKeys.includes(key); + const TextComponent = nonActionable ? StrikedText : Text; + return ( + + + {key} + + + {inferType(value) === FIELD_TYPE_OBJECT + ? deserializeOrDash(value) + : value} + + {nonActionable ? ( + + ) : ( + handleUniteParameter(key)}> + + + )} + + ); + })} + + ), + [maybeTemplate, inputParamsKeys, handleUniteParameter, copyAllValues], + ); + + return ( + + + Input templates (Default key-values from task definitions) + + + + } + accordionAdditionalProps={{ defaultExpanded: true }} + > + + + + These are default inputs that will be provided into your task unless + its overridden with an input parameter. To edit + these default input templates - click to{" "} + + + Edit task definition + + + {isFetching ? ( + + + + ) : ( + withTemplateKeysContent + )} + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SimpleTaskForm/index.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SimpleTaskForm/index.ts new file mode 100644 index 0000000000..bbaa5c7e52 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SimpleTaskForm/index.ts @@ -0,0 +1 @@ +export * from "./SimpleTaskForm"; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SimpleTaskNameInput.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SimpleTaskNameInput.tsx new file mode 100644 index 0000000000..62f61ffa02 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SimpleTaskNameInput.tsx @@ -0,0 +1,370 @@ +import Autocomplete, { createFilterOptions } from "@mui/material/Autocomplete"; +import Box from "@mui/material/Box"; +import CircularProgress from "@mui/material/CircularProgress"; +import Dialog from "@mui/material/Dialog"; +import DialogActions from "@mui/material/DialogActions"; +import DialogContent from "@mui/material/DialogContent"; +import DialogContentText from "@mui/material/DialogContentText"; +import DialogTitle from "@mui/material/DialogTitle"; +import Snackbar from "@mui/material/Snackbar"; +import { useSelector } from "@xstate/react"; +import MuiAlert from "components/MuiAlert"; +import Button from "components/MuiButton"; +import ConductorInput from "components/v1/ConductorInput"; +import XCloseIcon from "components/v1/icons/XCloseIcon"; +import { WorkflowEditContext } from "pages/definition/state"; +import React, { + FunctionComponent, + useCallback, + useContext, + useMemo, + useState, +} from "react"; +import { Link } from "react-router"; +import { autocompleteStyle } from "shared/styles"; +import { NEW_TASK_TEMPLATE } from "templates/JSONSchemaWorkflow"; +import { fontWeights } from "theme/tokens/variables"; +import { TaskDef, WorkflowDef } from "types"; +import { handleValidChars, handleValidCharsForEvents } from "utils"; +import { featureFlags, FEATURES } from "utils/flags"; +import { useAction, useFetch } from "utils/query"; + +const filter = createFilterOptions(); + +interface SimpleTaskNameInputProps { + onChange?: any; + value?: string; + error?: boolean; + helperText?: string; + isMetaBarEditing?: boolean; + triggerSuccessEvent?: () => void; +} + +const SimpleTaskNameInput: FunctionComponent = ({ + onChange, + value, + error, + helperText, + triggerSuccessEvent, +}: SimpleTaskNameInputProps) => { + const [dialogValue, setDialogValue] = useState<{ + name?: string; + inputValue?: string; + description?: string; + }>({ + name: "", + inputValue: "", + description: "", + }); + + const [open, toggleOpen] = useState(false); + const [maybeFormError, setFormError] = useState(""); + const [options, setOptions] = useState< + { name: string; description: string }[] + >([]); + + const { workflowDefinitionActor } = useContext(WorkflowEditContext); + const currentWorkflow = useSelector( + workflowDefinitionActor!, + (state) => state.context.currentWf, + ); + + const taskVisibility = featureFlags.getValue( + FEATURES.TASK_VISIBILITY, + "READ", + ); + const { refetch: refetchAllDefinitions } = useFetch( + `/metadata/taskdefs?access=${taskVisibility}`, + { + onSuccess: (data: TaskDef[]) => { + setOptions( + data + .map((task: TaskDef) => ({ + name: task.name, + description: task.description, + })) + .sort((a, b) => a.name.localeCompare(b.name)), + ); + }, + }, + ); + + const handleClose = () => { + setDialogValue({ + name: "", + inputValue: "", + description: "", + }); + + toggleOpen(false); + }; + + const { + mutate: persistNewTaskDefinition, + isLoading: isSavingNewTaskDefintion, + } = useAction("/metadata/taskdefs", "post", { + onSuccess: () => { + if (triggerSuccessEvent) { + triggerSuccessEvent(); + } + + refetchAllDefinitions(); + handleClose(); + }, + onError: (err: any) => { + console.error("There was an error", err); + setFormError("Error creating task definition"); + }, + }); + + const ownerEmail = useMemo( + () => (currentWorkflow as WorkflowDef).ownerEmail, + [currentWorkflow], + ); + + const maybeNameErrorProps = useMemo( + () => + options.findIndex(({ name }) => name === dialogValue.name) === -1 + ? {} + : { error: true, helperText: "Name already exists" }, + [options, dialogValue], + ); + + const handleSubmit = useCallback( + (event: any) => { + event.preventDefault(); + onChange(dialogValue.name); + + const newTaskDefinition = { + ...NEW_TASK_TEMPLATE, + ownerEmail, + ...dialogValue, + }; + // @ts-ignore + persistNewTaskDefinition({ + body: JSON.stringify([newTaskDefinition]), + }); + }, + [dialogValue, persistNewTaskDefinition, ownerEmail, onChange], + ); + + const isValidTaskDefinition = useMemo( + () => options?.some((option) => option?.name === value), + [value, options], + ); + + return ( + <> + + option?.name === currentValue + } + autoHighlight + componentsProps={{ paper: { elevation: 3 } }} + onChange={(_event, newValue: any) => { + if (typeof newValue === "string") { + // From Material docs: + // timeout to avoid instant validation of the dialog's form. + setTimeout(() => { + toggleOpen(true); + setDialogValue({ + name: newValue, + }); + }); + } else if (newValue && newValue.inputValue) { + toggleOpen(true); + setDialogValue({ + name: newValue.inputValue, + }); + } else { + if (typeof newValue?.name === "undefined") { + onChange(""); + } else { + onChange(newValue.name); + } + } + }} + filterOptions={(options, params) => { + const filtered = filter(options, params); + const currentValue = params.inputValue || value; + + if (currentValue) { + const currentIndex = options.findIndex( + (option) => option.name === currentValue, + ); + + if (currentIndex === -1) { + filtered.unshift({ + inputValue: currentValue, + name: `Create new: "${currentValue}"`, + }); + } + } + + return filtered; + }} + id="simple-task-name-input-autocomplete-text-field" + options={options} + getOptionLabel={(option) => { + // e.g value selected with enter, right from the input + if (typeof option === "string") { + return option; + } + if (option.inputValue) { + return option.inputValue; + } + return option.name; + }} + selectOnFocus + clearOnBlur + handleHomeEndKeys + renderOption={(props, option) => ( +
  • + + + {option.name} + + {option.description} + +
  • + )} + freeSolo + renderInput={(params) => { + const { inputProps: originalInputProps, ...restParams } = params; + const { + onChange: originalOnChange = ( + _e: React.ChangeEvent, + ) => ({}), + ...restInputProps + } = originalInputProps; + + return ( + + ); + }} + sx={[autocompleteStyle({ value })]} + clearIcon={} + /> + {value && isValidTaskDefinition && ( + + + Edit task definition + + + )} + +
    + Create task definition + + setFormError("")} + > + {maybeFormError} + + + A new task definition with default values will be created. + + + setDialogValue({ + ...dialogValue, + name: value, + }), + )} + label="Name" + type="text" + {...maybeNameErrorProps} + /> + + setDialogValue({ + ...dialogValue, + description: value, + }) + } + label="Description" + type="text" + /> + + + + + + {isSavingNewTaskDefintion && ( + + )} + + +
    +
    + + ); +}; + +export default SimpleTaskNameInput; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/StartWorkflowTaskForm/StartWorkflowTaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/StartWorkflowTaskForm/StartWorkflowTaskForm.tsx new file mode 100644 index 0000000000..e8ceebb6bf --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/StartWorkflowTaskForm/StartWorkflowTaskForm.tsx @@ -0,0 +1,301 @@ +import { Box, CircularProgress, Grid } from "@mui/material"; +import { useInterpret } from "@xstate/react"; +import Button from "components/MuiButton"; +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import { ConductorFlatMapFormBase } from "components/v1/FlatMapForm/ConductorFlatMapForm"; +import _path from "lodash/fp/path"; +import _isEmpty from "lodash/isEmpty"; +import _isNil from "lodash/isNil"; +import _last from "lodash/last"; +import { getWorkflowDefinitionByNameAndVersion } from "pages/definition/commonService"; +import TaskFormSection from "pages/definition/EditorPanel/TaskFormTab/forms/TaskFormSection"; +import { TaskFormProps } from "pages/definition/EditorPanel/TaskFormTab/forms/types"; +import IdempotencyForm from "pages/runWorkflow/IdempotencyForm"; +import { IdempotencyStrategyEnum } from "pages/runWorkflow/types"; +import { useMemo } from "react"; +import { TaskType } from "types/common"; +import { WORKFLOW_DEFINITION_URL } from "utils/constants/route"; +import { updateField } from "utils/fieldHelpers"; +import { openInNewTab } from "utils/helpers"; +import { useAuthHeaders } from "utils/query"; +import { + handleChangeIdempotencyValues, + updateInputParametersCommon, +} from "../../helpers"; +import { MaybeVariable } from "../MaybeVariable"; +import { Optional } from "../OptionalFieldForm"; +import { + StartSubWfNameVersionMachineContext, + startSubWfNameVersionMachine, +} from "./state"; +import { useStartSubWfNameVersionMachine } from "./state/hook"; + +const START_WORKFLOW_INPUT_PATH = "inputParameters.startWorkflow.input"; +const START_WORKFLOW_CORRELATION_ID_PATH = + "inputParameters.startWorkflow.correlationId"; + +export const StartWorkflowTaskForm = ({ task, onChange }: TaskFormProps) => { + const authHeaders = useAuthHeaders(); + + const maybeSelectedWorkflowName = useMemo( + () => + _isNil(task?.inputParameters?.startWorkflow?.name) || + _isEmpty(task?.inputParameters?.startWorkflow?.name) + ? undefined + : task?.inputParameters?.startWorkflow?.name, + [task?.inputParameters?.startWorkflow?.name], + ); + + const handleSelectServiceWhichCallsOnChange = ( + onChange: TaskFormProps["onChange"], + ) => { + return async (context: StartSubWfNameVersionMachineContext) => { + const taskJson = { + ...task, + inputParameters: { + ...task.inputParameters, + startWorkflow: { + ...task.inputParameters?.startWorkflow, + name: context.workflowName, + version: _last( + _path(context.workflowName, context.fetchedNamesAndVersions), + ), + input: {}, + }, + }, + }; + await updateInputParametersCommon( + taskJson, + task, + authHeaders, + onChange, + "inputParameters.startWorkflow", + "inputParameters.startWorkflow.input", + TaskType.START_WORKFLOW, + getWorkflowDefinitionByNameAndVersion, + ); + }; + }; + + const startSubWfNameVersionActor = useInterpret( + startSubWfNameVersionMachine, + { + ...(process.env.NODE_ENV === "development" ? { devTools: true } : {}), + context: { + authHeaders, + workflowName: maybeSelectedWorkflowName, + }, + services: { + handleSelect: handleSelectServiceWhichCallsOnChange(onChange), + }, + }, + ); + + const [ + { wfNameOptions: options, availableVersions, isFetching }, + { handleSelectWorkflowName }, + ] = useStartSubWfNameVersionMachine(startSubWfNameVersionActor); + + const isOpenButtonDisabled = useMemo( + () => + !( + task?.inputParameters?.startWorkflow?.name && + options?.includes(task?.inputParameters?.startWorkflow?.name) && + task?.inputParameters?.startWorkflow?.version && + !isFetching + ), + [ + task?.inputParameters?.startWorkflow?.version, + isFetching, + task?.inputParameters?.startWorkflow?.name, + options, + ], + ); + + return ( + + + + + + + { + handleSelectWorkflowName(val); + }} + onBlur={(val) => { + if (task?.inputParameters?.startWorkflow?.name !== val) { + handleSelectWorkflowName(val); + } + }} + onInputChange={(val) => { + onChange( + updateField( + "inputParameters.startWorkflow.name", + val, + task, + ), + ); + }} + value={task?.inputParameters?.startWorkflow?.name} + otherOptions={options} + label="Workflow name" + /> + + + { + const taskJson = { + ...task, + inputParameters: { + ...task.inputParameters, + startWorkflow: { + ...task.inputParameters?.startWorkflow, + version: val, + }, + }, + }; + updateInputParametersCommon( + taskJson, + task, + authHeaders, + onChange, + "inputParameters.startWorkflow", + "inputParameters.startWorkflow.input", + TaskType.START_WORKFLOW, + getWorkflowDefinitionByNameAndVersion, + ); + }} + value={task?.inputParameters?.startWorkflow?.version} + otherOptions={availableVersions} + label="Version" + coerceTo="integer" + /> + + + + + + + + + + + onChange( + updateField( + START_WORKFLOW_CORRELATION_ID_PATH, + val, + task, + ), + ) + } + /> + + + + + + + handleChangeIdempotencyValues( + data, + task, + "inputParameters.startWorkflow", + onChange, + ) + } + /> + + + + + + { + onChange(updateField(START_WORKFLOW_INPUT_PATH, val, task)); + }} + path={"inputParameters.startWorkflow.input"} + taskType={TaskType.START_WORKFLOW} + > + + onChange(updateField(START_WORKFLOW_INPUT_PATH, val, task)) + } + /> + + + + + + + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/StartWorkflowTaskForm/index.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/StartWorkflowTaskForm/index.ts new file mode 100644 index 0000000000..2d43bfd97c --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/StartWorkflowTaskForm/index.ts @@ -0,0 +1 @@ +export * from "./StartWorkflowTaskForm"; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/StartWorkflowTaskForm/state/actions.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/StartWorkflowTaskForm/state/actions.ts new file mode 100644 index 0000000000..70e8bbbf96 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/StartWorkflowTaskForm/state/actions.ts @@ -0,0 +1,43 @@ +import { DoneInvokeEvent, assign } from "xstate"; +import _keys from "lodash/keys"; +import _isEmpty from "lodash/isEmpty"; +import _isUndefined from "lodash/isUndefined"; +import _path from "lodash/fp/path"; +import { + SelectWorkflowNameEvent, + StartSubWfNameVersionMachineContext, +} from "./types"; + +export const persistWfName = assign< + StartSubWfNameVersionMachineContext, + SelectWorkflowNameEvent +>((_, { name }) => ({ + workflowName: name, +})); + +export const persistFetchedNamesAndVersions = assign< + StartSubWfNameVersionMachineContext, + DoneInvokeEvent> +>((_, { data }: { data: Map }) => { + const obj = Object.fromEntries(data.entries()); + return { + fetchedNamesAndVersions: obj, + }; +}); + +export const persistOptions = assign( + (context) => { + const namesAndVersinKeys = _keys(context?.fetchedNamesAndVersions); + const wfNameOptions = + namesAndVersinKeys.length === 0 ? [] : namesAndVersinKeys; + + const availableVersions = + _isUndefined(context.workflowName) && !_isEmpty(wfNameOptions) + ? [] + : _path(context.workflowName, context.fetchedNamesAndVersions); + return { + wfNameOptions: wfNameOptions, + availableVersions: availableVersions, + }; + }, +); diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/StartWorkflowTaskForm/state/hook.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/StartWorkflowTaskForm/state/hook.ts new file mode 100644 index 0000000000..3f3d0652fe --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/StartWorkflowTaskForm/state/hook.ts @@ -0,0 +1,46 @@ +import { ActorRef } from "xstate"; +import { useSelector } from "@xstate/react"; +import { + StartSubWfNameVersionEvents, + StartSubWfNameVersionStates, + StartSubWfNameVersionTypes, +} from "./types"; + +export const useStartSubWfNameVersionMachine = ( + actor: ActorRef, +) => { + const wfNameOptions = useSelector( + actor, + (state) => state.context.wfNameOptions, + ); + + const availableVersions = useSelector( + actor, + (state) => state.context.availableVersions, + ); + + const isFetching = useSelector( + actor, + (state) => + state.matches(StartSubWfNameVersionStates.HANDLE_SELECT_WORKFLOW_NAME) || + state.matches(StartSubWfNameVersionStates.GO_BACK_TO_IDLE), + ); + + const handleSelectWorkflowName = (name: string) => { + actor.send({ + type: StartSubWfNameVersionTypes.SELECT_WORKFLOW_NAME, + name, + }); + }; + + return [ + { + wfNameOptions, + availableVersions, + isFetching, + }, + { + handleSelectWorkflowName, + }, + ] as const; +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/StartWorkflowTaskForm/state/index.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/StartWorkflowTaskForm/state/index.ts new file mode 100644 index 0000000000..79fd82215f --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/StartWorkflowTaskForm/state/index.ts @@ -0,0 +1,2 @@ +export * from "./machine"; +export * from "./types"; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/StartWorkflowTaskForm/state/machine.test.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/StartWorkflowTaskForm/state/machine.test.ts new file mode 100644 index 0000000000..3fb9717059 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/StartWorkflowTaskForm/state/machine.test.ts @@ -0,0 +1,86 @@ +import { interpret } from "xstate"; +import { startSubWfNameVersionMachine } from "./machine"; +import { + StartSubWfNameVersionTypes, + StartSubWfNameVersionStates, +} from "./types"; +import * as actions from "./actions"; +// Mocking services + +const workflowNameVersionMap = new Map([ + ["workflow1", [1, 2]], + ["English_Lesson", [1]], + ["workflow13", [3, 4]], +]); +const mockMachine = startSubWfNameVersionMachine.withConfig({ + services: { + fetchWfNamesAndVersions: async () => + await Promise.resolve(workflowNameVersionMap), + handleSelect: async () => {}, + }, + actions: actions as any, +}); + +const service = interpret(mockMachine); + +beforeAll(() => { + // Start the service + service.start(); +}); + +afterAll(() => { + // Stop the service when you are no longer using it. + service.stop(); +}); +describe("StartSubWfVersion machine tests", () => { + it(`should reach ${StartSubWfNameVersionStates.IDLE} state after handling`, () => { + const newFieldName = "English_Lesson"; + + return new Promise((resolve, reject) => { + service.onTransition((state) => { + if ( + state.matches([StartSubWfNameVersionStates.IDLE]) && + !state.context.availableVersions + ) { + // Send events + service.send({ + type: StartSubWfNameVersionTypes.SELECT_WORKFLOW_NAME, + name: newFieldName, + }); + } + + try { + // When SELECT_WORKFLOW_NAME event occurs, state should be in HANDLE_SELECT_WORKFLOW_NAME + expect( + state.event.type !== + StartSubWfNameVersionTypes.SELECT_WORKFLOW_NAME || + state.matches([ + StartSubWfNameVersionStates.HANDLE_SELECT_WORKFLOW_NAME, + ]), + ).toBeTruthy(); + + // Check if we've reached the final state + const isIdleWithVersions = + state.matches([StartSubWfNameVersionStates.IDLE]) && + state.context.availableVersions; + + // Assert context values are correct when in final state + expect( + !isIdleWithVersions || state.context.workflowName === newFieldName, + ).toBeTruthy(); + expect( + !isIdleWithVersions || + JSON.stringify(state.context.availableVersions) === + JSON.stringify([1]), + ).toBeTruthy(); + + if (isIdleWithVersions) { + resolve(); + } + } catch (error) { + reject(error); + } + }); + }); + }); +}); diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/StartWorkflowTaskForm/state/machine.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/StartWorkflowTaskForm/state/machine.ts new file mode 100644 index 0000000000..639f348811 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/StartWorkflowTaskForm/state/machine.ts @@ -0,0 +1,60 @@ +import { createMachine } from "xstate"; +import * as actions from "./actions"; +import * as services from "./service"; +import { + StartSubWfNameVersionEvents, + StartSubWfNameVersionMachineContext, + StartSubWfNameVersionStates, + StartSubWfNameVersionTypes, +} from "./types"; + +export const startSubWfNameVersionMachine = createMachine< + StartSubWfNameVersionMachineContext, + StartSubWfNameVersionEvents +>( + { + id: "startSubWfNameVersionMachine", + predictableActionArguments: true, + initial: "initial", + context: { + authHeaders: {}, + workflowName: "", + fetchedNamesAndVersions: undefined, + }, + states: { + initial: { + invoke: { + id: "fetchWfNamesAndVersions", + src: "fetchWfNamesAndVersions", + onDone: { + actions: ["persistFetchedNamesAndVersions", "persistOptions"], + target: StartSubWfNameVersionStates.IDLE, + }, + }, + }, + [StartSubWfNameVersionStates.IDLE]: { + on: { + [StartSubWfNameVersionTypes.SELECT_WORKFLOW_NAME]: { + actions: ["persistWfName"], + target: StartSubWfNameVersionStates.HANDLE_SELECT_WORKFLOW_NAME, + }, + }, + }, + [StartSubWfNameVersionStates.HANDLE_SELECT_WORKFLOW_NAME]: { + invoke: { + id: "handleSelect", + src: "handleSelect", + onDone: { + actions: ["persistOptions"], + target: StartSubWfNameVersionStates.IDLE, + }, + onError: {}, + }, + }, + }, + }, + { + actions: actions as any, + services: services as any, + }, +); diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/StartWorkflowTaskForm/state/service.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/StartWorkflowTaskForm/state/service.ts new file mode 100644 index 0000000000..0ccc9564f9 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/StartWorkflowTaskForm/state/service.ts @@ -0,0 +1,26 @@ +import { StartSubWfNameVersionMachineContext } from "./types"; + +import { queryClient } from "queryClient"; +import { fetchWithContext, fetchContextNonHook } from "plugins/fetch"; + +import { logger } from "utils/logger"; +import { getUniqueWorkflowsWithVersions } from "utils/workflow"; +import { WORKFLOW_METADATA_BASE_URL_SHORT } from "utils/constants/api"; + +const fetchContext = fetchContextNonHook(); + +export const fetchWfNamesAndVersions = async ({ + authHeaders: headers, +}: StartSubWfNameVersionMachineContext) => { + const url = WORKFLOW_METADATA_BASE_URL_SHORT; + try { + const response = await queryClient.fetchQuery( + [fetchContext.stack, url], + () => fetchWithContext(url, fetchContext, { headers }), + ); + return getUniqueWorkflowsWithVersions(response); + } catch (error) { + logger.error("Fetching Wf short", error); + return Promise.reject({ message: "Error fetching wf short" }); + } +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/StartWorkflowTaskForm/state/types.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/StartWorkflowTaskForm/state/types.ts new file mode 100644 index 0000000000..b24e6cdfaf --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/StartWorkflowTaskForm/state/types.ts @@ -0,0 +1,25 @@ +import { AuthHeaders } from "types/common"; + +export enum StartSubWfNameVersionTypes { + SELECT_WORKFLOW_NAME = "SELECT_WORKFLOW_NAME", +} +export enum StartSubWfNameVersionStates { + IDLE = "IDLE", + HANDLE_SELECT_WORKFLOW_NAME = "HANDLE_SELECT_WORKFLOW_NAME", + GO_BACK_TO_IDLE = "GO_BACK_TO_IDLE", +} + +export type SelectWorkflowNameEvent = { + type: StartSubWfNameVersionTypes.SELECT_WORKFLOW_NAME; + name: string; +}; + +export type StartSubWfNameVersionEvents = SelectWorkflowNameEvent; + +export interface StartSubWfNameVersionMachineContext { + authHeaders: AuthHeaders; + workflowName: string; + fetchedNamesAndVersions?: Record; + wfNameOptions?: string[]; + availableVersions?: number[]; +} diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SubWorkflowOperatorForm/SubWorkflowOperatorForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SubWorkflowOperatorForm/SubWorkflowOperatorForm.tsx new file mode 100644 index 0000000000..82369ef695 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SubWorkflowOperatorForm/SubWorkflowOperatorForm.tsx @@ -0,0 +1,356 @@ +import { Box, CircularProgress, FormControlLabel, Grid } from "@mui/material"; +import { useInterpret } from "@xstate/react"; +import Button from "components/MuiButton"; +import MuiCheckbox from "components/MuiCheckbox"; +import { ConductorAutoComplete } from "components/v1/ConductorAutoComplete"; +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import { ConductorFlatMapFormBase } from "components/v1/FlatMapForm/ConductorFlatMapForm"; +import { path as _path } from "lodash/fp"; +import _isEmpty from "lodash/isEmpty"; +import _isNil from "lodash/isNil"; +import _last from "lodash/last"; +import { getWorkflowDefinitionByNameAndVersion } from "pages/definition/commonService"; +import IdempotencyForm from "pages/runWorkflow/IdempotencyForm"; +import { IdempotencyStrategyEnum } from "pages/runWorkflow/types"; +import { useMemo } from "react"; +import { TaskType } from "types"; +import { WORKFLOW_DEFINITION_URL } from "utils/constants/route"; +import { updateField } from "utils/fieldHelpers"; +import { useAuthHeaders } from "utils/query"; +import { + handleChangeIdempotencyValues, + updateInputParametersCommon, +} from "../../helpers"; +import { ConductorObjectOrStringInput } from "../ConductorObjectOrStringInput"; +import { Optional } from "../OptionalFieldForm"; +import { + StartSubWfNameVersionMachineContext, + startSubWfNameVersionMachine, +} from "../StartWorkflowTaskForm/state"; +import { useStartSubWfNameVersionMachine } from "../StartWorkflowTaskForm/state/hook"; +import TaskFormSection from "../TaskFormSection"; +import { TaskFormProps } from "../types"; + +const SUB_WORKFLOW_INPUT_PARAMETER_PATH = "inputParameters"; +const SUB_WORKFLOW_TASK_TO_DOMAIN_PATH = "subWorkflowParam.taskToDomain"; + +export const SubWorkflowOperatorForm = ({ + task, + onChange, + onToggleExpand, + collapseWorkflowList, +}: TaskFormProps) => { + const authHeaders = useAuthHeaders(); + + const maybeSelectedWorkflowName = useMemo( + () => + _isNil(task?.subWorkflowParam?.name) || + _isEmpty(task?.subWorkflowParam?.name) + ? undefined + : task?.subWorkflowParam?.name, + [task?.subWorkflowParam?.name], + ); + + const handleSelectServiceWhichCallsOnChange = ( + onChange: TaskFormProps["onChange"], + ) => { + return async (context: StartSubWfNameVersionMachineContext) => { + const taskJson = { + ...task, + inputParameters: {}, + subWorkflowParam: { + ...task.subWorkflowParam, + name: context.workflowName, + version: _last( + _path(context.workflowName, context.fetchedNamesAndVersions), + ) as number, + }, + }; + + await updateInputParametersCommon( + taskJson, + task, + authHeaders, + onChange, + "subWorkflowParam", + "inputParameters", + TaskType.SUB_WORKFLOW, + getWorkflowDefinitionByNameAndVersion, + ); + }; + }; + + const startSubWfNameVersionActor = useInterpret( + startSubWfNameVersionMachine, + { + ...(process.env.NODE_ENV === "development" ? { devTools: true } : {}), + context: { + authHeaders, + workflowName: maybeSelectedWorkflowName, + }, + services: { + handleSelect: handleSelectServiceWhichCallsOnChange(onChange), + }, + }, + ); + + const [ + { wfNameOptions: options, availableVersions, isFetching }, + { handleSelectWorkflowName }, + ] = useStartSubWfNameVersionMachine(startSubWfNameVersionActor); + + const isOpenButtonDisabled = useMemo( + () => + !( + task?.subWorkflowParam?.name && + options?.includes(task?.subWorkflowParam?.name) && + task?.subWorkflowParam?.version && + !isFetching + ), + [ + task?.subWorkflowParam?.name, + options, + task?.subWorkflowParam?.version, + isFetching, + ], + ); + + const isPriorityError = + typeof task?.subWorkflowParam?.priority === "number" && + (task.subWorkflowParam.priority < 0 || task.subWorkflowParam.priority > 99); + return ( + + + + + + + { + handleSelectWorkflowName(val); + }} + onBlur={(val) => { + if (task.subWorkflowParam?.name !== val) { + handleSelectWorkflowName(val); + } + }} + onInputChange={(val) => { + onChange(updateField("subWorkflowParam.name", val, task)); + }} + value={task.subWorkflowParam?.name} + otherOptions={options} + label="Workflow name" + /> + + + { + // Convert the value to an integer if it's not already + const version = + typeof val === "string" ? parseInt(val, 10) : val; + + const taskJson = { + ...task, + subWorkflowParam: { + ...task.subWorkflowParam, + version, + }, + }; + updateInputParametersCommon( + taskJson, + task, + authHeaders, + onChange, + "subWorkflowParam", + "inputParameters", + TaskType.SUB_WORKFLOW, + getWorkflowDefinitionByNameAndVersion, + ); + }} + value={task.subWorkflowParam?.version} + options={availableVersions} + label="Version" + /> + + + + + + + + + { + onChange( + updateField( + "subWorkflowParam.workflowDefinition", + val, + task, + ), + ); + }} + /> + + + + onChange( + updateField("subWorkflowParam.priority", val, task), + ) + } + error={isPriorityError} + helperText={ + isPriorityError ? "must be from 0 to 99" : undefined + } + coerceTo="integer" + inputProps={{ + tooltip: { + title: "Priority", + content: + "If set, this priority overrides the parent workflow’s priority. If not, it inherits the parent workflow’s priority.", + }, + }} + /> + + + + + + + handleChangeIdempotencyValues( + data, + task, + "subWorkflowParam", + onChange, + ) + } + /> + + + + + + onToggleExpand(task?.subWorkflowParam?.name) + } + /> + } + label="Expand" + /> + + + + + + + + onChange( + updateField(SUB_WORKFLOW_INPUT_PARAMETER_PATH, value, task), + ) + } + /> + + + + onChange(updateField(SUB_WORKFLOW_TASK_TO_DOMAIN_PATH, value, task)) + } + /> + + + + + + + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SubWorkflowOperatorForm/index.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SubWorkflowOperatorForm/index.ts new file mode 100644 index 0000000000..476bc4fa51 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SubWorkflowOperatorForm/index.ts @@ -0,0 +1 @@ +export * from "./SubWorkflowOperatorForm"; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SubWorkflowOperatorForm/types.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SubWorkflowOperatorForm/types.ts new file mode 100644 index 0000000000..fd6b415bac --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SubWorkflowOperatorForm/types.ts @@ -0,0 +1,3 @@ +import { WorkflowDef } from "types"; + +export type WorkflowByName = Record; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SwitchTaskForm/SwitchCodeBlock.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SwitchTaskForm/SwitchCodeBlock.tsx new file mode 100644 index 0000000000..29a56836e3 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SwitchTaskForm/SwitchCodeBlock.tsx @@ -0,0 +1,213 @@ +import { EditorProps, Monaco } from "@monaco-editor/react"; +import { BoxProps } from "@mui/material"; +import { Theme } from "@mui/material/styles"; +import { SxProps } from "@mui/system"; +import _keys from "lodash/keys"; +import { + CSSProperties, + FunctionComponent, + MutableRefObject, + ReactNode, + useCallback, + useContext, + useEffect, + useRef, +} from "react"; + +import { ConductorCodeBlockInput } from "components/v1/ConductorCodeBlockInput"; +import { + invalidDollarVariables, + undeclaredInputParameters, +} from "pages/definition/helpers"; +import { ColorModeContext } from "theme/material/ColorModeContext"; +import { SwitchTaskDef } from "types"; +import { + OnlyTheWordInfoProp, + editorAddCommandAltEnter, + editorDecorations, +} from "../../helpers"; +import { smallEditorDefaultOptions } from "../editorConfig"; +import { logger } from "utils/logger"; + +type SwitchCodeBlockProps = { + label?: ReactNode; + language?: string; + onChange?: (taskChanges: Partial) => void; + containerProps?: BoxProps; + error?: boolean; + height?: number | "auto"; + minHeight?: number; + autoformat?: boolean; + labelStyle?: SxProps; + languageLabel?: string; + containerStyles?: CSSProperties; + autoSizeBox?: boolean; + task: Partial; +} & Partial>; + +const additionalEditorOptions = { + lineNumbers: "on" as const, + lineDecorationsWidth: 10, +}; + +const warnUndeclaredVariables = ( + editor: Monaco, + monaco: any, + task: Partial, + currentDecorations: MutableRefObject, +) => { + const model = editor.getModel(); + const taskExpression = task?.expression; + if (model && taskExpression && editor) { + const addedInputParameters = undeclaredInputParameters( + model.getValue(), + task?.inputParameters, + ); + + const invalidDollarVars = invalidDollarVariables(model.getValue()); + + const decorations = editorDecorations( + model, + [...addedInputParameters, ...invalidDollarVars], + monaco, + ); + + return editor.deltaDecorations( + currentDecorations.current ? currentDecorations.current : [], + decorations.flat(), + ); + } +}; + +const SwitchCodeBlock: FunctionComponent = ({ + language = "json", + onChange = () => null, + autoSizeBox = false, + task, + ...restOfProps +}) => { + const taskRef = useRef | null>(null); + taskRef.current = task; + const { mode } = useContext(ColorModeContext); + const disposeRef = useRef(null) as any; + const currentDecorations = useRef([]) as any; + + useEffect(() => { + return () => { + if (disposeRef.current) { + try { + disposeRef.current(); + } catch (error) { + logger.error("Error disposing from Ref on unmount", error); + } + } + }; + }, []); + + const handleEditorDidMount = useCallback( + (editor: Monaco, monaco: any) => { + const callBackFunction = (onlyTheWordInfo: OnlyTheWordInfoProp) => { + onChange({ + ...taskRef.current, + inputParameters: { + ...taskRef.current!.inputParameters, + [onlyTheWordInfo.word]: "", // Add the original word + }, + } as Partial); + // cleanup + currentDecorations.current = warnUndeclaredVariables( + editor, + monaco, + taskRef.current!, + currentDecorations, + ); + }; + // editor.AddCommand function + editorAddCommandAltEnter(editor, monaco, taskRef, callBackFunction); + + editor.onDidChangeModelContent((_event: any) => { + // Warn on change + currentDecorations.current = warnUndeclaredVariables( + editor, + monaco, + taskRef.current!, + currentDecorations, + ); + }); + + // Warn on mount + currentDecorations.current = warnUndeclaredVariables( + editor, + monaco, + taskRef.current!, + currentDecorations, + ); + }, + [onChange], + ); + + const onEditorChange = useCallback( + (editorValue: string) => { + onChange({ + ...taskRef.current, + expression: editorValue, + } as Partial); + }, + [onChange], + ); + + return ( + { + handleEditorDidMount(editor, monaco); + }} + beforeMount={(monaco: Monaco) => { + if (disposeRef.current) { + try { + disposeRef.current(); + } catch (error) { + logger.error("Error disposing from Ref on beforeMount", error); + } + disposeRef.current = null; + } + const disposable = monaco.languages.registerCompletionItemProvider( + "javascript", + { + provideCompletionItems: () => { + const inputVariables = _keys(taskRef?.current?.inputParameters); + let variableSuggestions: string[] = []; + if (inputVariables) { + variableSuggestions = inputVariables.map((item) => `$.${item}`); + } + // Provide suggestions for JSON properties that start with the current text + const propertySuggestions = variableSuggestions.map( + (property) => ({ + label: property, + kind: monaco.languages.CompletionItemKind.Value, + insertText: `${property}`, + }), + ); + // Merge custom suggestions with JSON property suggestions + const suggestions = [...propertySuggestions]; + return { suggestions }; + }, + }, + ); + + disposeRef.current = () => disposable.dispose(); + }} + defaultLanguage={language} + options={{ + ...smallEditorDefaultOptions, + ...(autoSizeBox && { scrollBeyondLastLine: false }), + ...additionalEditorOptions, + }} + value={taskRef?.current?.expression || ""} + {...restOfProps} + /> + ); +}; + +export default SwitchCodeBlock; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SwitchTaskForm/SwitchOperatorForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SwitchTaskForm/SwitchOperatorForm.tsx new file mode 100644 index 0000000000..00717f4100 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SwitchTaskForm/SwitchOperatorForm.tsx @@ -0,0 +1,230 @@ +import { Box, FormControlLabel, Grid } from "@mui/material"; +import { useSelector } from "@xstate/react"; +import MuiCheckbox from "components/MuiCheckbox"; +import RadioButtonGroup from "components/RadioButtonGroup"; +import ConfirmChoiceDialog from "components/ConfirmChoiceDialog"; +import ConductorInput from "components/v1/ConductorInput"; +import { ConductorFlatMapFormBase } from "components/v1/FlatMapForm/ConductorFlatMapForm"; +import _path from "lodash/fp/path"; +import _isEmpty from "lodash/isEmpty"; +import _nth from "lodash/nth"; +import { useCallback, useContext, useState } from "react"; +import { colors } from "theme/tokens/variables"; +import { SwitchTaskDef } from "types/TaskType"; +import { filterOptionByEvaluatorType } from "utils/deprecatedRadioFilter"; +import { updateField } from "utils/fieldHelpers"; +import { TaskFormContext } from "../../state"; +import { Optional } from "../OptionalFieldForm"; +import TaskFormSection from "../TaskFormSection"; +import { TaskFormProps } from "../types"; +import { useGetSetHandler } from "../useGetSetHandler"; +import SwitchCodeBlock from "./SwitchCodeBlock"; + +const EXPRESSION_PATH = "expression"; +const DECISION_CASES_PATH = "decisionCases"; + +export const SwitchOperatorForm = (props: TaskFormProps) => { + const { task, onChange } = props; + const { formTaskActor } = useContext(TaskFormContext); + + const [desicionCases, handleDesicionCases] = useGetSetHandler( + props, + DECISION_CASES_PATH, + ); + + const maybeSelectedBranch = useSelector( + formTaskActor!, + (state) => state.context.maybeSelectedSwitchBranch, + ); + + const firstInputParameterKey = _nth( + Object.keys(task?.inputParameters ?? {}), + 0, + ); + + const [showConfirmOverrideDialog, setShowConfirmOverrideDialog] = + useState(false); + + const radioOptions = filterOptionByEvaluatorType(task?.evaluatorType); + const DEFAULT_EXPRESSION = `(function () { + switch ($.${firstInputParameterKey ?? "switchCaseValue"}) { + case "1": + return "switch_case"; + case "2": + return "switch_case_1"; + case "3": + return "switch_case_2" + } + }())`; + + const handleApplySampleScript = useCallback(() => { + onChange({ + ...task, + expression: DEFAULT_EXPRESSION, + ...(_isEmpty(task?.decisionCases) + ? { + decisionCases: { + switch_case: [], + switch_case_1: [], + switch_case_2: [], + }, + } + : {}), + }); + }, [task, onChange, DEFAULT_EXPRESSION]); + + const isSingleInputParam = + Object.keys({ ...task.inputParameters }).length === 1; + + const handleUpdateValueParam = (value: string) => { + if (isSingleInputParam) { + const existingParamValue = + (task.expression && task.inputParameters?.[task.expression]) || ""; + onChange({ + ...task, + expression: value, + inputParameters: { + [value]: existingParamValue, + }, + }); + } else { + onChange({ ...task, expression: value }); + } + }; + const onInputParameterChange = (newValue: Record) => + onChange(updateField("inputParameters", newValue, task)); + + return ( + + + + + + + + + + + + { + onChange(updateField("evaluatorType", value, task)); + }} + /> + } + label="Evaluate:" + sx={{ + marginLeft: 0, + "& .MuiFormControlLabel-label": { + fontWeight: 600, + color: colors.gray07, + }, + }} + /> + + + {["javascript", "graaljs"].includes( + task.evaluatorType as string, + ) ? ( + <> + + setShowConfirmOverrideDialog(true)} + control={ + + } + label={"Use sample script"} + style={{ margin: 0 }} + /> + + } + onChange={onChange} + /> + + ) : ( + + )} + + + + + + + + + + + + + + + + + {showConfirmOverrideDialog && ( + { + if (confirmed) { + handleApplySampleScript(); + } + setShowConfirmOverrideDialog(false); + }} + message={ + "Applying the sample script will overwrite any existing script. Are you sure you want to proceed?" + } + /> + )} + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SwitchTaskForm/index.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SwitchTaskForm/index.ts new file mode 100644 index 0000000000..7a23dbe968 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/SwitchTaskForm/index.ts @@ -0,0 +1 @@ +export * from "./SwitchOperatorForm"; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TaskFormFooter.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TaskFormFooter.tsx new file mode 100644 index 0000000000..aae3ea39b9 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TaskFormFooter.tsx @@ -0,0 +1,196 @@ +import { useMemo } from "react"; +import { Box, FormControlLabel, Switch, Grid } from "@mui/material"; +import { colors } from "theme/tokens/variables"; +import JSONField from "./JSONField"; +import ArrayForm from "./ArrayForm"; +import { Input, Dropdown } from "../../../../../components"; + +import TaskFormSection from "./TaskFormSection"; +import MuiTypography from "components/MuiTypography"; + +type Props = { + selectedNode: any; + onChange: any; +}; + +const TaskFormFooter = ({ selectedNode, onChange }: Props) => { + const currentTask = useMemo(() => selectedNode?.data?.task, [selectedNode]); + return ( + + + Additional Parameters + + + + + + + + } + label="Optional" + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default TaskFormFooter; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TaskFormHeader/TaskFormHeader.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TaskFormHeader/TaskFormHeader.tsx new file mode 100644 index 0000000000..89756bf1bd --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TaskFormHeader/TaskFormHeader.tsx @@ -0,0 +1,54 @@ +import { Paper } from "@mui/material"; +import { useSelector } from "@xstate/react"; +import { FunctionComponent } from "react"; +import { ActorRef } from "xstate"; + +import { TaskType } from "types"; +import { TaskHeaderMachineEvents } from "./state/types"; +import { TaskFormHeaderSimple } from "./TaskFormHeaderSimple"; +import { TaskFormHeaderTasks } from "./TaskFormHeaderTasks"; +import { featureFlags, FEATURES } from "utils/flags"; + +export interface TaskFormHeaderProps { + taskFormHeaderActor: ActorRef; +} + +const showServiceTemplateSelector = featureFlags.isEnabled( + FEATURES.REMOTE_SERVICES, +); +// TODO we should probably have two different components when for simple and one for the other... Two many ifs +const TaskFormHeader: FunctionComponent = ({ + taskFormHeaderActor, +}) => { + const taskType = useSelector( + taskFormHeaderActor, + (state) => state.context.taskType, + ); + + const tasksArrayWithTaskDropdown = [ + TaskType.SIMPLE, + TaskType.HUMAN, + ...(showServiceTemplateSelector ? [TaskType.HTTP, TaskType.GRPC] : []), + ]; + + return ( + theme.palette.customBackground.form, + }} + > + {tasksArrayWithTaskDropdown.includes(taskType) ? ( + + ) : ( + + )} + + ); +}; + +export default TaskFormHeader; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TaskFormHeader/TaskFormHeaderSimple.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TaskFormHeader/TaskFormHeaderSimple.tsx new file mode 100644 index 0000000000..3c70764185 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TaskFormHeader/TaskFormHeaderSimple.tsx @@ -0,0 +1,129 @@ +import { Grid, Paper } from "@mui/material"; +import { useSelector } from "@xstate/react"; +import { FunctionComponent } from "react"; +import { ActorRef } from "xstate"; + +import { Button } from "components"; +import ConductorInput from "components/v1/ConductorInput"; +import SimpleTaskNameInput from "../SimpleTaskNameInput"; +import { + TaskFormHeaderEventTypes, + TaskHeaderMachineEvents, +} from "./state/types"; + +export interface TaskFormHeaderSimpleProps { + taskFormHeaderActor: ActorRef; +} + +export const TaskFormHeaderSimple: FunctionComponent< + TaskFormHeaderSimpleProps +> = ({ taskFormHeaderActor }) => { + const taskName = useSelector( + taskFormHeaderActor, + (state) => state.context.name, + ); + const taskReferenceName = useSelector( + taskFormHeaderActor, + (state) => state.context.taskReferenceName, + ); + + const send = taskFormHeaderActor.send; + + const handleGenerateNameTaskReferenceName = () => { + send({ + type: TaskFormHeaderEventTypes.GENERATE_TASK_REFERENCE_NAME, + }); + }; + const handleChangeName = (value: string) => { + send({ + type: TaskFormHeaderEventTypes.CHANGE_NAME_VALUE, + value, + }); + }; + + const handleChangeTaskReferenceName = (value: string) => { + send({ + type: TaskFormHeaderEventTypes.CHANGE_TASK_REFERENCE_VALUE, + value, + }); + }; + + const triggerSuccessEvent = () => { + send({ + type: TaskFormHeaderEventTypes.TASK_CREATED_SUCCESSFULLY, + }); + }; + + return ( + theme.palette.customBackground.form, + }} + > + + + + + + { + handleChangeTaskReferenceName(value); + }} + onFocus={() => + send({ type: TaskFormHeaderEventTypes.START_EDITING_VALUES }) + } + onBlur={() => + send({ type: TaskFormHeaderEventTypes.STOP_EDITING_VALUES }) + } + /> + + + + + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TaskFormHeader/TaskFormHeaderTasks.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TaskFormHeader/TaskFormHeaderTasks.tsx new file mode 100644 index 0000000000..4c3c4e45d2 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TaskFormHeader/TaskFormHeaderTasks.tsx @@ -0,0 +1,109 @@ +import { Grid } from "@mui/material"; +import { Theme } from "@mui/material/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import { useActor, useSelector } from "@xstate/react"; +import { FunctionComponent } from "react"; +import { ActorRef } from "xstate"; + +import { Button } from "components"; +import ConductorInput from "components/v1/ConductorInput"; +import { + TaskFormHeaderEventTypes, + TaskHeaderMachineEvents, +} from "./state/types"; + +export interface TaskFormHeaderTasksProps { + taskFormHeaderActor: ActorRef; +} +export const TaskFormHeaderTasks: FunctionComponent< + TaskFormHeaderTasksProps +> = ({ taskFormHeaderActor }) => { + const isMobileWidth = useMediaQuery((theme: Theme) => + theme.breakpoints.down("sm"), + ); + + const taskName = useSelector( + taskFormHeaderActor, + (state) => state.context.name, + ); + const taskReferenceName = useSelector( + taskFormHeaderActor, + (state) => state.context.taskReferenceName, + ); + + const [, send] = useActor(taskFormHeaderActor); + + const handleChangeName = (value: string) => { + send({ + type: TaskFormHeaderEventTypes.CHANGE_NAME_VALUE, + value, + }); + }; + const handleGenerateNameTaskReferenceName = () => { + send({ + type: TaskFormHeaderEventTypes.GENERATE_TASK_REFERENCE_NAME, + }); + }; + + const handleChangeTaskReferenceName = (value: string) => { + send({ + type: TaskFormHeaderEventTypes.CHANGE_TASK_REFERENCE_VALUE, + value, + }); + }; + + return ( + + + { + handleChangeName(value); + }} + onFocus={() => + send({ type: TaskFormHeaderEventTypes.START_EDITING_VALUES }) + } + onBlur={() => + send({ type: TaskFormHeaderEventTypes.STOP_EDITING_VALUES }) + } + /> + + + { + handleChangeTaskReferenceName(value); + }} + onFocus={() => + send({ type: TaskFormHeaderEventTypes.START_EDITING_VALUES }) + } + onBlur={() => + send({ type: TaskFormHeaderEventTypes.STOP_EDITING_VALUES }) + } + /> + + + + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TaskFormHeader/state/actions.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TaskFormHeader/state/actions.ts new file mode 100644 index 0000000000..6676232808 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TaskFormHeader/state/actions.ts @@ -0,0 +1,83 @@ +import { assign, sendParent } from "xstate"; +import { cancel } from "xstate/lib/actions"; +import { + TaskFormHeaderMachineContext, + ChangeNameValueEvent, + ValuesUpdatedEvent, + StartEditingValuesEvent, + StopEditingValuesEvent, +} from "./types"; +import { FormMachineActionTypes } from "pages/definition/EditorPanel/TaskFormTab/state/types"; + +export const persistNameChanges = assign< + TaskFormHeaderMachineContext, + ChangeNameValueEvent +>({ + name: (_context, { value }) => value, +}); + +export const persistTaskReferenceNameChanges = assign< + TaskFormHeaderMachineContext, + ChangeNameValueEvent +>({ + taskReferenceName: (_context, { value }) => value, +}); + +export const persistChanges = assign< + TaskFormHeaderMachineContext, + ValuesUpdatedEvent +>({ + taskReferenceName: (_context, { taskReferenceName }) => taskReferenceName, + name: (_context, { name }) => name, + taskType: (_context, { taskType }) => taskType, +}); + +export const syncWithParent = sendParent( + ({ name, taskReferenceName }: TaskFormHeaderMachineContext) => ({ + type: FormMachineActionTypes.UPDATE_TASK, + taskChanges: { name, taskReferenceName }, + }), +); + +const referenceNameGenerator = ( + name: string, + taskReferenceName: string, + suffix: string, +) => { + if (taskReferenceName === `${name}_ref`) { + return `${name}_${suffix}_ref`; + } else { + return `${name}_ref`; + } +}; + +export const generateTaskReferenceAndName = assign< + TaskFormHeaderMachineContext, + any +>(({ name, taskReferenceName, taskType }) => { + const suffix = Math.random().toString(36).substring(2, 5); + const taskName = name ? name : `${taskType.toLowerCase()}`; + const refName = name + ? referenceNameGenerator(name, taskReferenceName, suffix) + : `${taskType.toLowerCase()}_ref`; + return { + name: taskName, + taskReferenceName: refName, + }; +}); + +export const cancelSyncWithParent = cancel("sync_val_with_parent"); + +export const startEditingValues = assign< + TaskFormHeaderMachineContext, + StartEditingValuesEvent +>({ + isEditingValues: true, +}); + +export const stopEditingValues = assign< + TaskFormHeaderMachineContext, + StopEditingValuesEvent +>({ + isEditingValues: false, +}); diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TaskFormHeader/state/hook.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TaskFormHeader/state/hook.ts new file mode 100644 index 0000000000..c369cfeb70 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TaskFormHeader/state/hook.ts @@ -0,0 +1,35 @@ +import { ActorRef } from "xstate"; +import { useSelector } from "@xstate/react"; +import { WorkflowMetadataEvents } from "pages/definition/WorkflowMetadata/state"; + +export const useWorkflowMetadataEditorActor = ( + metadataEditorActor: ActorRef, +) => { + const [ + inputParametersActor, + outputParametersActors, + restartableActors, + timeoutSecondsActors, + timeoutPolicyActors, + failureWorkflowActors, + ] = useSelector( + metadataEditorActor, + (state) => state.context.editableFieldActors, + ); + + const isReady = useSelector(metadataEditorActor, (state) => + state.hasTag("editingEnabled"), + ); + + return [ + { + inputParametersActor, + outputParametersActors, + restartableActors, + timeoutSecondsActors, + timeoutPolicyActors, + failureWorkflowActors, + isReady, + }, + ]; +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TaskFormHeader/state/index.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TaskFormHeader/state/index.ts new file mode 100644 index 0000000000..4ef79909e8 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TaskFormHeader/state/index.ts @@ -0,0 +1,3 @@ +export * from "./types"; +export * from "./machine"; +export * from "./hook"; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TaskFormHeader/state/machine.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TaskFormHeader/state/machine.ts new file mode 100644 index 0000000000..b4953331bf --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TaskFormHeader/state/machine.ts @@ -0,0 +1,58 @@ +import { TaskType } from "types"; +import { createMachine } from "xstate"; +import * as actions from "./actions"; +import { + TaskFormHeaderMachineContext, + TaskFormHeaderEventTypes, + TaskHeaderMachineEvents, +} from "./types"; + +export const taskFormHeaderMachine = createMachine< + TaskFormHeaderMachineContext, + TaskHeaderMachineEvents +>( + { + id: "taskFormHeaderMachine", + initial: "focused", + predictableActionArguments: true, + context: { + name: "", + taskReferenceName: "", + taskType: TaskType.SIMPLE, + }, + on: { + [TaskFormHeaderEventTypes.VALUES_UPDATED]: { + // Only persist changes if not typing in the task reference name or name, + // otherwise there is a race condition with + // CHANGE_TASK_REFERENCE_VALUE or CHANGE_NAME_VALUE + cond: (ctx) => !ctx.isEditingValues, + actions: ["persistChanges"], + }, + [TaskFormHeaderEventTypes.TASK_CREATED_SUCCESSFULLY]: {}, + }, + states: { + focused: { + on: { + [TaskFormHeaderEventTypes.START_EDITING_VALUES]: { + actions: ["startEditingValues"], + }, + [TaskFormHeaderEventTypes.STOP_EDITING_VALUES]: { + actions: ["stopEditingValues"], + }, + [TaskFormHeaderEventTypes.CHANGE_TASK_REFERENCE_VALUE]: { + actions: ["persistTaskReferenceNameChanges", "syncWithParent"], + }, + [TaskFormHeaderEventTypes.CHANGE_NAME_VALUE]: { + actions: ["persistNameChanges", "syncWithParent"], + }, + [TaskFormHeaderEventTypes.GENERATE_TASK_REFERENCE_NAME]: { + actions: ["generateTaskReferenceAndName", "syncWithParent"], + }, + }, + }, + }, + }, + { + actions: actions as any, + }, +); diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TaskFormHeader/state/types.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TaskFormHeader/state/types.ts new file mode 100644 index 0000000000..0d852c8fd0 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TaskFormHeader/state/types.ts @@ -0,0 +1,58 @@ +import { TaskType } from "types"; + +export interface TaskFormHeaderMachineContext { + name: string; + taskReferenceName: string; + taskType: TaskType; + isEditingValues?: boolean; +} + +export enum TaskFormHeaderEventTypes { + CHANGE_NAME_VALUE = "CHANGE_NAME_VALUE", + CHANGE_TASK_REFERENCE_VALUE = "CHANGE_TASK_REFERENCE_VALUE", + VALUES_UPDATED = "VALUES_UPDATED", + GENERATE_TASK_REFERENCE_NAME = "GENERATE_TASK_REFERENCE_NAME", + TASK_CREATED_SUCCESSFULLY = "TASK_CREATED_SUCCESSFULLY", + START_EDITING_VALUES = "START_EDITING_VALUES", + STOP_EDITING_VALUES = "STOP_EDITING_VALUES", +} + +export type StartEditingValuesEvent = { + type: TaskFormHeaderEventTypes.START_EDITING_VALUES; +}; +export type StopEditingValuesEvent = { + type: TaskFormHeaderEventTypes.STOP_EDITING_VALUES; +}; +export type ChangeNameValueEvent = { + type: TaskFormHeaderEventTypes.CHANGE_NAME_VALUE; + value: string; +}; + +export type ChangeTaskReferenceNameValueEvent = { + type: TaskFormHeaderEventTypes.CHANGE_TASK_REFERENCE_VALUE; + value: string; +}; + +export type ValuesUpdatedEvent = { + type: TaskFormHeaderEventTypes.VALUES_UPDATED; + taskType: TaskType; + name: string; + taskReferenceName: string; +}; + +export type GenerateTaskNameReferenceNameEvent = { + type: TaskFormHeaderEventTypes.GENERATE_TASK_REFERENCE_NAME; +}; + +export type TaskCreatedSucceffullyEvent = { + type: TaskFormHeaderEventTypes.TASK_CREATED_SUCCESSFULLY; +}; + +export type TaskHeaderMachineEvents = + | ChangeNameValueEvent + | ChangeTaskReferenceNameValueEvent + | GenerateTaskNameReferenceNameEvent + | ValuesUpdatedEvent + | TaskCreatedSucceffullyEvent + | StartEditingValuesEvent + | StopEditingValuesEvent; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TaskFormSection.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TaskFormSection.tsx new file mode 100644 index 0000000000..6c8692c3fe --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TaskFormSection.tsx @@ -0,0 +1,168 @@ +import { Box } from "@mui/material"; +import Accordion, { AccordionProps } from "@mui/material/Accordion"; +import AccordionDetails from "@mui/material/AccordionDetails"; +import AccordionSummary from "@mui/material/AccordionSummary"; +import { CaretDown } from "@phosphor-icons/react"; +import MuiTypography from "components/MuiTypography"; +import _isString from "lodash/isString"; +import React, { useContext } from "react"; +import { ColorModeContext } from "theme/material/ColorModeContext"; + +type TaskFormSectionProps = { + title?: React.ReactNode; + children: React.ReactNode; + collapsible?: boolean; + accordionAdditionalProps?: Partial; +}; + +type TaskFormSectionAccordionProps = { + title?: React.ReactNode; + collapsible?: boolean; + accordionAdditionalProps?: Partial; + children?: React.ReactNode; +}; + +const MaybeWrappedTitle = ({ title }: { title: React.ReactNode }) => + _isString(title) ? ( + + {title} + + ) : ( + <>{title} + ); + +const TaskFormAccordion = ({ + title, + accordionAdditionalProps, + children, +}: TaskFormSectionAccordionProps) => { + const { mode } = useContext(ColorModeContext); + + const ACCORDION_HEIGHT = 40; + + const keyTitle = _isString(title) ? title : ""; + + return ( + + + } + aria-controls={`${keyTitle}-content`} + id={`${keyTitle}-header`} + > + {title ? : null} + + + {children} + + + ); +}; + +const TaskFormSection = ({ + title, + children, + collapsible = false, + accordionAdditionalProps = {}, +}: TaskFormSectionProps) => { + return collapsible ? ( + + + {children} + + + ) : ( + + {title ? : null} + {children} + + ); +}; + +export default TaskFormSection; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TaskFormStyles.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TaskFormStyles.ts new file mode 100644 index 0000000000..bf1e1cdc06 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TaskFormStyles.ts @@ -0,0 +1,99 @@ +import sharedStyles from "pages/styles"; + +// @ts-ignore-line +export const style = { + ...sharedStyles, + paper: { + margin: "20px", + padding: "20px", + }, + name: { + width: "50%", + }, + submitButton: { + float: "right", + }, + fields: { + display: "flex", + flexDirection: "column", + gap: "15px", + }, + controls: { + marginLeft: "15px", + marginTop: "20px", + height: "calc(100% - 83px)", + overflowY: "scroll", + width: "calc(100% - 15px)", + overflowX: "hidden", + paddingBottom: "60px", + }, + monaco: { + padding: "10px", + borderColor: "rgba(128, 128, 128, 0.2)", + borderStyle: "solid", + borderWidth: "1px", + borderRadius: "4px", + backgroundColor: "rgb(255, 255, 255)", + "&:focus-within": { + margin: "-2px", + borderColor: "rgb(73, 105, 228)", + borderStyle: "solid", + borderWidth: "2px", + }, + }, + labelText: { + position: "relative", + fontSize: "13px", + transform: "none", + fontWeight: 600, + paddingLeft: 0, + paddingBottom: "8px", + }, + inputBox: { + marginTop: "10px", + "& textarea": { + minWidth: "368px", + fontFamily: "monospace", + }, + "& input": { + minWidth: "368px", + }, + "& label": {}, + }, + roBox: { + marginTop: "10px", + "& .MuiOutlinedInput-root": { + background: "transparent", + border: "none", + }, + "& fieldset": { + border: "none", + }, + "& textarea": { + minWidth: "450px", + minHeight: "140px", + fontFamily: "monospace", + overflow: "none", + }, + "& input": { + minWidth: "368px", + }, + "& label": {}, + }, + cronApply: { + marginTop: "-12px", + "& svg": { + fontSize: "18px", + }, + }, + toggleButton: { + marginTop: "-12px", + "& svg": { + fontSize: "22px", + }, + }, + cronSample: { + fontSize: "12px", + height: "50px", + }, +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TerminateOperatorForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TerminateOperatorForm.tsx new file mode 100644 index 0000000000..197f8bcc49 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TerminateOperatorForm.tsx @@ -0,0 +1,78 @@ +import { Grid, Stack } from "@mui/material"; +import { ConductorAutoComplete } from "components/v1"; +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import { ConductorFlatMapFormBase } from "components/v1/FlatMapForm/ConductorFlatMapForm"; +import { path as _path } from "lodash/fp"; +import { updateField } from "utils/fieldHelpers"; +import TaskFormSection from "./TaskFormSection"; +import { TaskFormProps } from "./types"; +import { useGetSetHandler } from "./useGetSetHandler"; + +const terminationStatusPath = "inputParameters.terminationStatus"; +const terminationReasonPath = "inputParameters.terminationReason"; +const workflowOutputPath = "inputParameters.workflowOutput"; + +export const TerminateOperatorForm = (props: TaskFormProps) => { + const { task, onChange } = props; + + const [terminationReason, setTerminationReason] = useGetSetHandler( + props, + terminationReasonPath, + ); + + return ( + + + + + + onChange(updateField(terminationStatusPath, value, task)) + } + /> + + + + + + + + + + + onChange(updateField(workflowOutputPath, value, task)) + } + /> + + + + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TerminateWorkflowForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TerminateWorkflowForm.tsx new file mode 100644 index 0000000000..b86a405c9a --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TerminateWorkflowForm.tsx @@ -0,0 +1,80 @@ +import { Box, FormControlLabel, Grid } from "@mui/material"; +import MuiCheckbox from "components/MuiCheckbox"; +import { AutocompleteArrayField } from "components/v1/FlatMapForm/ConductorAutocompleteArrayField"; +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import { TaskType } from "types/common"; +import { Optional } from "./OptionalFieldForm"; +import TaskFormSection from "./TaskFormSection"; +import { TaskFormProps } from "./types"; +import { useGetSetHandler } from "./useGetSetHandler"; + +const workFlowId = "inputParameters.workflowId"; +const terminationReasonPath = "inputParameters.terminationReason"; +const triggerFailureWorkflowPath = "inputParameters.triggerFailureWorkflow"; + +export const TerminateWorkflowForm = (props: TaskFormProps) => { + const { task, onChange } = props; + const triggerHandleChange = () => { + setTriggerFailureWorkflow(!triggerFailureWorkflow); + }; + + const [workFlowIds, handleWorkFlowIds] = useGetSetHandler(props, workFlowId); + + const [terminationReason, setTerminationReason] = useGetSetHandler( + props, + terminationReasonPath, + ); + + const [triggerFailureWorkflow, setTriggerFailureWorkflow] = useGetSetHandler( + props, + triggerFailureWorkflowPath, + ); + + return ( + + + + + + + + + + + + + + + + + + } + label={"Trigger Failure Workflow"} + /> + + + + + + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TestTaskButton/OpenTestTaskButton.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TestTaskButton/OpenTestTaskButton.tsx new file mode 100644 index 0000000000..a43153dec7 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TestTaskButton/OpenTestTaskButton.tsx @@ -0,0 +1,35 @@ +import { useState } from "react"; +import { Button } from "@mui/material"; +import { TestTaskButton } from "./TestTaskButton"; +import { OpenTestTaskButtonProps } from "types/TestTaskTypes"; +import { RocketLaunch } from "@mui/icons-material"; + +export const OpenTestTaskButton = ({ + task, + maxHeight, + disabled = false, + showForm = true, + tasksList = [], +}: OpenTestTaskButtonProps) => { + const [open, setOpen] = useState(false); + return open ? ( + setOpen(false)} + task={task} + showForm={showForm} + maxHeight={maxHeight} + tasksList={tasksList} + /> + ) : ( + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TestTaskButton/TestTaskButton.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TestTaskButton/TestTaskButton.tsx new file mode 100644 index 0000000000..bfc94a702b --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TestTaskButton/TestTaskButton.tsx @@ -0,0 +1,75 @@ +import { TestTask } from "components/v1/TestTask"; +import { useInterpret, useSelector } from "@xstate/react"; +import { TestTaskButtonMachineStates, TestTaskMachine } from "./state"; +import { useAuthHeaders } from "utils/query"; +import { useTestTaskButtonMachine } from "./state/hook"; +import { useAuth } from "shared/auth"; +import { useContext } from "react"; +import { MessageContext } from "components/v1/layout/MessageContext"; +import { TestTaskButtonProps } from "types/TestTaskTypes"; + +export const TestTaskButton = ({ + task, + maxHeight, + onDismiss, + showForm, + tasksList, +}: TestTaskButtonProps) => { + const authHeaders = useAuthHeaders(); + const { conductorUser } = useAuth(); + const { setMessage } = useContext(MessageContext); + + const testTaskActor = useInterpret(TestTaskMachine, { + ...(process.env.NODE_ENV === "development" ? { devTools: true } : {}), + context: { + authHeaders, + user: conductorUser, + originalTask: task, + taskChanges: task?.inputParameters, + tasksList: tasksList, + }, + actions: { + setErrorMessage: (__, data: any) => { + setMessage({ + text: data?.data?.message, + severity: "error", + }); + }, + }, + }); + + const [ + { taskChanges, taskDomain, testExecutionId, testedTaskExecutionResult }, + { setInputParameters, setTaskDomain, handleRunTestTask }, + ] = useTestTaskButtonMachine(testTaskActor); + + const isInProgress = useSelector(testTaskActor, (state) => { + return ( + state.matches([ + TestTaskButtonMachineStates.RUN_TEST_TASK, + "runTestTask", + ]) || + state.matches([ + TestTaskButtonMachineStates.RUN_TEST_TASK, + "pollForExecutionResult", + ]) + ); + }); + + return ( + setInputParameters(value)} + domain={taskDomain} + onChangeDomain={(value) => setTaskDomain(value)} + value={taskChanges} + maxHeight={maxHeight} + handleRunTestTask={handleRunTestTask} + testExecutionId={testExecutionId} + onDismiss={onDismiss} + testedTaskExecutionResult={testedTaskExecutionResult} + isInProgress={isInProgress} + showForm={showForm} + /> + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TestTaskButton/index.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TestTaskButton/index.ts new file mode 100644 index 0000000000..3e17c5a56d --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TestTaskButton/index.ts @@ -0,0 +1,3 @@ +import { TestTaskButton } from "./TestTaskButton"; + +export default TestTaskButton; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TestTaskButton/state/actions.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TestTaskButton/state/actions.ts new file mode 100644 index 0000000000..8c4fae5f6d --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TestTaskButton/state/actions.ts @@ -0,0 +1,33 @@ +import { DoneInvokeEvent, assign } from "xstate"; +import { + SetTaskDomainEvent, + UpdateTaskVariablesEvent, + TestTaskButtonMachineContext, +} from "./types"; +import { Execution } from "types/Execution"; + +export const setTaskDomain = assign< + TestTaskButtonMachineContext, + SetTaskDomainEvent +>((_, { domain }) => ({ + taskDomain: domain, +})); + +export const persistTaskChanges = assign< + TestTaskButtonMachineContext, + UpdateTaskVariablesEvent +>((_, { inputParameters }) => ({ + taskChanges: inputParameters, +})); + +export const persistExecutionId = assign< + TestTaskButtonMachineContext, + DoneInvokeEvent +>({ + testExecutionId: (_context, { data }) => data, +}); + +export const persistTestedTaskExecutionResult = assign< + TestTaskButtonMachineContext, + DoneInvokeEvent +>((_context, { data }) => ({ testedTaskExecutionResult: data })); diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TestTaskButton/state/hook.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TestTaskButton/state/hook.ts new file mode 100644 index 0000000000..02002c0751 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TestTaskButton/state/hook.ts @@ -0,0 +1,55 @@ +import { ActorRef } from "xstate"; +import { useSelector } from "@xstate/react"; +import { TestTaskButtonTypes, TestTaskButtonEvents } from "./types"; + +export const useTestTaskButtonMachine = ( + actor: ActorRef, +) => { + const originalTask = useSelector( + actor, + (state) => state.context.originalTask, + ); + const taskChanges = useSelector(actor, (state) => state.context.taskChanges); + const taskDomain = useSelector(actor, (state) => state.context.taskDomain); + const testedTaskExecutionResult = useSelector( + actor, + (state) => state.context.testedTaskExecutionResult, + ); + const testExecutionId = useSelector( + actor, + (state) => state.context.testExecutionId, + ); + + const setInputParameters = (inputParameters: Record) => { + actor.send({ + type: TestTaskButtonTypes.UPDATE_TASK_VARIABLES, + inputParameters, + }); + }; + const setTaskDomain = (domain: string) => { + actor.send({ + type: TestTaskButtonTypes.SET_TASK_DOMAIN, + domain, + }); + }; + const handleRunTestTask = () => { + actor.send({ + type: TestTaskButtonTypes.TEST_TASK, + }); + }; + + return [ + { + originalTask, + taskChanges, + taskDomain, + testedTaskExecutionResult, + testExecutionId, + }, + { + setInputParameters, + setTaskDomain, + handleRunTestTask, + }, + ] as const; +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TestTaskButton/state/index.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TestTaskButton/state/index.ts new file mode 100644 index 0000000000..79fd82215f --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TestTaskButton/state/index.ts @@ -0,0 +1,2 @@ +export * from "./machine"; +export * from "./types"; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TestTaskButton/state/machine.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TestTaskButton/state/machine.ts new file mode 100644 index 0000000000..70578c0175 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TestTaskButton/state/machine.ts @@ -0,0 +1,103 @@ +import { createMachine, assign } from "xstate"; +import * as customActions from "./actions"; +import * as services from "./service"; +import { + TestTaskButtonTypes, + TestTaskButtonMachineContext, + TestTaskButtonEvents, + TestTaskButtonMachineStates, +} from "./types"; + +export const TestTaskMachine = createMachine< + TestTaskButtonMachineContext, + TestTaskButtonEvents +>( + { + id: "testTaskMachine", + predictableActionArguments: true, + initial: "configuringTask", + context: { + originalTask: {}, + taskChanges: {}, + testedTaskExecutionResult: {}, + authHeaders: {}, + tasksList: [], + }, + states: { + configuringTask: { + on: { + [TestTaskButtonTypes.SET_TASK_JSON]: { + actions: assign({ + originalTask: (_, event) => event.originalTask, + taskChanges: (_, event) => event.taskChanges, + }), + }, + [TestTaskButtonTypes.UPDATE_TASK_VARIABLES]: { + actions: ["persistTaskChanges"], + }, + [TestTaskButtonTypes.SET_TASK_DOMAIN]: { + actions: ["setTaskDomain"], + }, + [TestTaskButtonTypes.TEST_TASK]: { + target: TestTaskButtonMachineStates.RUN_TEST_TASK, + }, + }, + }, + [TestTaskButtonMachineStates.RUN_TEST_TASK]: { + initial: "runTestTask", + states: { + runTestTask: { + invoke: { + id: "runTestTask", + src: "runTestTask", + onDone: { + target: "pollForExecutionResult", + actions: ["persistExecutionId"], + }, + onError: { + target: "#testTaskMachine.configuringTask", + actions: ["setErrorMessage"], + }, + }, + }, + pollForExecutionResult: { + invoke: { + id: "pollForExecutionResult", + src: "pollForExecutionResult", + onDone: [ + { + cond: (_context, { data: { status } }) => + status === "RUNNING", + target: "keepPolling", + actions: ["persistTestedTaskExecutionResult"], + }, + { + target: "displayOutput", + actions: ["persistTestedTaskExecutionResult"], + }, + ], + onError: { + target: "#testTaskMachine.configuringTask", + actions: ["setErrorMessage"], + }, + }, + }, + keepPolling: { + after: { + 1000: { + target: "pollForExecutionResult", + }, + }, + }, + displayOutput: { + type: "final", + }, + }, + }, + }, + }, + { + actions: customActions as any, + services: services as any, + }, +); diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TestTaskButton/state/service.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TestTaskButton/state/service.ts new file mode 100644 index 0000000000..cc12c3a1a4 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TestTaskButton/state/service.ts @@ -0,0 +1,93 @@ +import { tryFunc, tryToJson } from "utils/utils"; +import { TestTaskButtonMachineContext } from "./types"; +import { fetchWithContext, fetchContextNonHook } from "plugins/fetch"; +import { queryClient } from "queryClient"; +import { logger } from "utils/logger"; +import { TaskType } from "types/common"; +import { getCorrespondingJoinTask } from "../../../helpers"; + +const fetchContext = fetchContextNonHook(); + +export const runTestTask = async ({ + authHeaders, + originalTask, + user, + taskDomain, + taskChanges, + tasksList, +}: TestTaskButtonMachineContext) => { + const suffix = Math.random().toString(36).substring(2, 9); + const name = `test_task_${originalTask?.name}_${suffix}`; + + const originalTaskIsFork = originalTask?.type === TaskType.FORK_JOIN; + const originalTaskIsDynamicFork = + originalTask?.type === TaskType.FORK_JOIN_DYNAMIC; + + const workflowWithTask = { + name: name, + version: 1, + workflowDef: { + name: name, + description: `Test ${originalTask?.type} task within a workflow`, + version: 1, + tasks: [ + { + ...originalTask, + inputParameters: + taskChanges && tryToJson(JSON.stringify(taskChanges)), + }, + ...(originalTaskIsFork || originalTaskIsDynamicFork + ? getCorrespondingJoinTask(originalTask ?? {}, tasksList) + : []), + ], + createdBy: user?.id || "example@email.com", + }, + ...(taskDomain + ? { + taskToDomain: { + [`${originalTask?.taskReferenceName}`]: taskDomain, + }, + } + : {}), + }; + + const body = JSON.stringify(workflowWithTask, null, 0); + + return tryFunc({ + fn: async () => { + return await fetchWithContext( + "/workflow", + {}, + { + method: "POST", + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + body, + }, + true, + ); + }, + customError: { + message: "Run test task failed.", + }, + showCustomError: false, + }); +}; + +export const pollForExecutionResult = async ({ + authHeaders: headers, + testExecutionId, +}: TestTaskButtonMachineContext) => { + const url = `/workflow/${testExecutionId}?summarize=true`; + try { + const result = await queryClient.fetchQuery([fetchContext.stack, url], () => + fetchWithContext(url, fetchContext, { headers }), + ); + return result; + } catch (error) { + logger.error("Fetching task list page", error); + return Promise.reject({ message: "Error fetching task list page" }); + } +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TestTaskButton/state/types.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TestTaskButton/state/types.ts new file mode 100644 index 0000000000..6196b82632 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/TestTaskButton/state/types.ts @@ -0,0 +1,49 @@ +import { User } from "types/User"; +import { AuthHeaders, TaskDef } from "types/common"; + +export enum TestTaskButtonTypes { + CHANGE_VALUE = "CHANGE_VALUE", + UPDATE_TASK_VARIABLES = "UPDATE_TASK_VARIABLES", + TEST_TASK = "TEST_TASK", + SET_TASK_DOMAIN = "SET_TASK_DOMAIN", + SET_TASK_JSON = "SET_TASK_JSON", + TOGGLE_TEST_TASK = "TOGGLE_TEST_TASK", +} + +export enum TestTaskButtonMachineStates { + RUN_TEST_TASK = "RUN_TEST_TASK", +} + +export type SetTaskJsonEvent = { + type: TestTaskButtonTypes.SET_TASK_JSON; + originalTask: Record; + taskChanges: Record; +}; +export type UpdateTaskVariablesEvent = { + type: TestTaskButtonTypes.UPDATE_TASK_VARIABLES; + inputParameters: Record; +}; +export type SetTaskDomainEvent = { + type: TestTaskButtonTypes.SET_TASK_DOMAIN; + domain: string; +}; +export type TestTaskEvent = { + type: TestTaskButtonTypes.TEST_TASK; +}; + +export type TestTaskButtonEvents = + | TestTaskEvent + | SetTaskDomainEvent + | UpdateTaskVariablesEvent + | SetTaskJsonEvent; + +export interface TestTaskButtonMachineContext { + originalTask?: Record; + taskChanges?: Record; + taskDomain?: string; + testedTaskExecutionResult?: Record; + authHeaders: AuthHeaders; + testExecutionId?: string; + user?: User; + tasksList?: Partial[]; +} diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/UnknownTaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/UnknownTaskForm.tsx new file mode 100644 index 0000000000..8f44131cdf --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/UnknownTaskForm.tsx @@ -0,0 +1,30 @@ +import TaskFormSection from "pages/definition/EditorPanel/TaskFormTab/forms/TaskFormSection"; +import { Box, Grid } from "@mui/material"; +import { TaskFormProps } from "pages/definition/EditorPanel/TaskFormTab/forms/types"; +import { ConductorFlatMapFormBase } from "components/v1/FlatMapForm/ConductorFlatMapForm"; + +export const UnknownTaskForm = ({ task, onChange }: TaskFormProps) => { + return ( + + + + + + onChange({ ...task, inputParameters: newParams }) + } + /> + + + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/UpdateSecretForm/UpdateSecretTaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/UpdateSecretForm/UpdateSecretTaskForm.tsx new file mode 100644 index 0000000000..f922bb0552 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/UpdateSecretForm/UpdateSecretTaskForm.tsx @@ -0,0 +1,62 @@ +import { Box, Grid } from "@mui/material"; +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import { TaskType } from "types"; +import { MaybeVariable } from "../MaybeVariable"; +import { Optional } from "../OptionalFieldForm"; +import TaskFormSection from "../TaskFormSection"; +import { useTaskForm } from "../hooks/useTaskForm"; +import { TaskFormProps } from "../types"; +import { useGetSetHandler } from "../useGetSetHandler"; + +const secretPath = "inputParameters._secrets"; +const secretKeyPath = `${secretPath}.secretKey`; +const secretValuePath = `${secretPath}.secretValue`; + +const UpdateSecretTaskForm = (props: TaskFormProps) => { + const { task, onChange } = props; + const [secretKey, setSecretKey] = useTaskForm(secretKeyPath, props); + const [secretValue, setSecretValue] = useTaskForm(secretValuePath, props); + const [secrets, handleSecrets] = useGetSetHandler(props, secretPath); + + return ( + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default UpdateSecretTaskForm; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/UpdateSecretForm/index.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/UpdateSecretForm/index.ts new file mode 100644 index 0000000000..31bdf9d513 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/UpdateSecretForm/index.ts @@ -0,0 +1,3 @@ +import UpdateSecretTaskForm from "./UpdateSecretTaskForm"; + +export { UpdateSecretTaskForm }; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/UpdateTaskForm/UpdateTaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/UpdateTaskForm/UpdateTaskForm.tsx new file mode 100644 index 0000000000..ed0e13ab01 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/UpdateTaskForm/UpdateTaskForm.tsx @@ -0,0 +1,95 @@ +import { Box, FormControlLabel, Grid } from "@mui/material"; + +import MuiCheckbox from "components/MuiCheckbox"; +import { + AnInputComponent, + EventJson, + UpdateTaskFormEvent, +} from "pages/definition/EditorPanel/TaskFormTab/forms/UpdateTaskForm/UpdateTaskFromEvent"; +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import { ConductorFlatMapFormBase } from "components/v1/FlatMapForm/ConductorFlatMapForm"; +import { updateField } from "utils/fieldHelpers"; +import TaskFormSection from "../TaskFormSection"; +import { TaskFormProps } from "../types"; +import { useUpdateTaskHandler } from "./common"; +import { UpdateTaskStatus } from "types/UpdateTaskStatus"; +import { Optional } from "../OptionalFieldForm"; + +export const UpdateTaskForm = ({ task, onChange }: TaskFormProps) => { + const { handleTaskStatusChange, handleMergeOutputChange } = + useUpdateTaskHandler({ + task, + onChange, + }); + return ( + + + + + { + onChange({ + ...task, + inputParameters: { + ...task.inputParameters, + ...{ + workflowId: value?.workflowId, + taskId: value?.taskId, + taskRefName: value?.taskRefName, + }, + }, + }); + }} + inputComponent={ + ConductorAutocompleteVariables as AnInputComponent + } + /> + + + handleTaskStatusChange(value)} + otherOptions={Object.values(UpdateTaskStatus)} + error={!task?.inputParameters?.taskStatus} + /> + + + + + } + label="Merge Output (append the output to existing output)" + /> + + + + + + onChange(updateField("inputParameters.taskOutput", value, task)) + } + /> + + + + + + + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/UpdateTaskForm/UpdateTaskFromEvent.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/UpdateTaskForm/UpdateTaskFromEvent.tsx new file mode 100644 index 0000000000..ae73fdc6d9 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/UpdateTaskForm/UpdateTaskFromEvent.tsx @@ -0,0 +1,130 @@ +import { FormControlLabel, Grid, Radio, RadioGroup } from "@mui/material"; +import _omit from "lodash/omit"; +import React, { useMemo } from "react"; + +import ConductorInput, { + ConductorInputProps, +} from "../../../../../../components/v1/ConductorInput"; +import { ConductorAutocompleteVariablesProps } from "../../../../../../components/v1/FlatMapForm/ConductorAutocompleteVariables"; + +type EventTaskReferenceInput = { taskId: string }; +type WorkflowTaskReferenceInput = { workflowId: string; taskRefName: string }; +export type EventJson = Partial< + EventTaskReferenceInput & WorkflowTaskReferenceInput +>; +export type AnInputComponent = React.FunctionComponent< + ConductorInputProps | ConductorAutocompleteVariablesProps +>; + +interface FormWithRadioGroupProps { + value: EventJson; + onChange: (value: EventJson) => void; + inputComponent?: AnInputComponent; +} + +const omitTaskId = (value: EventJson) => _omit(value, "taskId"); + +const omitWorkflowID = (value: EventJson) => + _omit(value, ["workflowId", "taskRefName"]); + +const _isTaskIdSelected = (value: EventJson) => value?.taskId != null; + +export const UpdateTaskFormEvent = ({ + value, + onChange, + inputComponent: InputComponent = ConductorInput as AnInputComponent, +}: FormWithRadioGroupProps) => { + const isTaskIdSelected = useMemo(() => _isTaskIdSelected(value), [value]); + + return ( + <> + label >span": { fontWeight: 600, mb: 2 } }} + name="refresh-radio-group-options" + row + value={isTaskIdSelected ? "task-id" : "workflow-id-task-ref"} + onChange={(event) => { + if (event.target.value === "task-id") { + onChange({ + taskId: value.taskId ?? "", + }); + } else { + onChange({ + workflowId: value.workflowId ?? "", + taskRefName: value.taskRefName ?? "", + }); + } + }} + > + } + label="Workflow Id + Task Ref Name" + value="workflow-id-task-ref" + id="workflow-and-task-ref-radio-button" + /> + } + label="Task ID" + value="task-id" + id="task-id-radio-button" + /> + + {isTaskIdSelected ? ( + + { + const newValue = + typeof val === "string" ? val : val?.target?.value; + + onChange(omitWorkflowID({ ...value, taskId: newValue })); + }} + /> + + ) : ( + + + { + const newValue = + typeof val === "string" ? val : val?.target?.value; + + onChange(omitTaskId({ ...value, workflowId: newValue })); + }} + /> + + + { + const newValue = + typeof val === "string" ? val : val?.target?.value; + + onChange(omitTaskId({ ...value, taskRefName: newValue })); + }} + /> + + + )} + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/UpdateTaskForm/common.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/UpdateTaskForm/common.ts new file mode 100644 index 0000000000..7cd8620c2e --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/UpdateTaskForm/common.ts @@ -0,0 +1,18 @@ +import { ChangeEvent } from "react"; +import { updateField } from "utils/fieldHelpers"; +import { TaskFormProps } from "../types"; + +export const useUpdateTaskHandler = ({ task, onChange }: TaskFormProps) => { + const handleTaskStatusChange = (value: string) => + onChange(updateField("inputParameters.taskStatus", value, task)); + + const handleMergeOutputChange = (event: ChangeEvent) => { + const isChecked = event.target.checked; + onChange(updateField("inputParameters.mergeOutput", isChecked, task)); + }; + + return { + handleTaskStatusChange, + handleMergeOutputChange, + }; +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/UpdateTaskForm/index.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/UpdateTaskForm/index.ts new file mode 100644 index 0000000000..41a8951e9f --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/UpdateTaskForm/index.ts @@ -0,0 +1 @@ +export * from "./UpdateTaskForm"; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/WaitForWebhookForm/WaitForWebhookTaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/WaitForWebhookForm/WaitForWebhookTaskForm.tsx new file mode 100644 index 0000000000..77cce60cf3 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/WaitForWebhookForm/WaitForWebhookTaskForm.tsx @@ -0,0 +1,44 @@ +import { Box, Grid } from "@mui/material"; + +import { ConductorFlatMapForm } from "components/v1/FlatMapForm/ConductorFlatMapForm"; +import TaskFormSection from "pages/definition/EditorPanel/TaskFormTab/forms/TaskFormSection"; +import { TaskFormProps } from "pages/definition/EditorPanel/TaskFormTab/forms/types"; +import { TaskType } from "types"; +import { Optional } from "../OptionalFieldForm"; +import { useGetSetHandler } from "../useGetSetHandler"; + +const MATCH_PATH = "inputParameters.matches"; +const WaitForWebhookTaskForm = (props: TaskFormProps) => { + const { task, onChange } = props; + const [match, handlerMatch] = useGetSetHandler(props, MATCH_PATH); + return ( + + + + + + + + + + + + + + + ); +}; + +export default WaitForWebhookTaskForm; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/WaitForWebhookForm/index.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/WaitForWebhookForm/index.ts new file mode 100644 index 0000000000..f71ac4da44 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/WaitForWebhookForm/index.ts @@ -0,0 +1,3 @@ +import WaitForWebhookTaskForm from "./WaitForWebhookTaskForm"; + +export default WaitForWebhookTaskForm; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/WaitTaskForm/DurationWaitTaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/WaitTaskForm/DurationWaitTaskForm.tsx new file mode 100644 index 0000000000..957dd32245 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/WaitTaskForm/DurationWaitTaskForm.tsx @@ -0,0 +1,92 @@ +import { Grid, Link } from "@mui/material"; +import MuiTypography from "components/MuiTypography"; +import ConductorInputNumber from "components/v1/ConductorInputNumber"; +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import _capitalize from "lodash/capitalize"; +import { durationStringToPairs } from "pages/definition/EditorPanel/TaskFormTab/forms/WaitTaskForm/helpers"; +import { FunctionComponent, useMemo } from "react"; + +const DurationWaitTaskForm: FunctionComponent<{ + value: string; + onChange: (val: string) => void; +}> = ({ value, onChange }) => { + const durationTuples = useMemo(() => { + return durationStringToPairs(value); + }, [value]); + + const handlePairUpdate = (idx: number) => (modTuple: [string, string]) => { + const currentTuples = [...durationTuples]; + + // update the latest change + currentTuples[idx] = modTuple; + + const durationString = currentTuples.reduce( + (acc, [value, unit]) => + Number(value) > 0 ? `${acc} ${value} ${unit}` : acc, + "", + ); + onChange(durationString.trim()); + }; + + return ( + + {durationTuples.map(([value, unit], idx) => ( + + + handlePairUpdate(idx)([`${newValue}`, unit]) + } + sx={{ + maxWidth: 80, + // ...disabledInputStyle, + }} + /> + + ))} + + = + + + + Variable  + + ( + + variables + +  is autofilled unless override) + + + } + InputLabelProps={{ + sx: { + pointerEvents: "auto", + }, + }} + value={value} + onChange={onChange} + /> + + + ); +}; + +export default DurationWaitTaskForm; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/WaitTaskForm/SelectWaitType.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/WaitTaskForm/SelectWaitType.tsx new file mode 100644 index 0000000000..0d8828170a --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/WaitTaskForm/SelectWaitType.tsx @@ -0,0 +1,71 @@ +import { FunctionComponent } from "react"; +import { WaitType } from "pages/definition/EditorPanel/TaskFormTab/forms/WaitTaskForm/types"; +import { FormControl } from "@mui/material"; +import { colors } from "theme/tokens/variables"; +import _capitalize from "lodash/capitalize"; +import Button from "components/MuiButton"; +import ButtonGroup from "components/MuiButtonGroup"; + +const SelectWaitType: FunctionComponent<{ + options: WaitType[]; + onChange: (val: WaitType) => void; + value: string; +}> = ({ options, onChange, value }) => { + return ( + + + {options.map((option) => ( + + ))} + + + ); +}; + +export default SelectWaitType; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/WaitTaskForm/UntilWaitTaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/WaitTaskForm/UntilWaitTaskForm.tsx new file mode 100644 index 0000000000..d96e166001 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/WaitTaskForm/UntilWaitTaskForm.tsx @@ -0,0 +1,162 @@ +import { Grid, Link } from "@mui/material"; +import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; +import MuiTypography from "components/MuiTypography"; +import { ConductorAutoComplete } from "components/v1"; +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import ConductorDateTimePicker from "components/v1/date-time/ConductorDateTimePicker"; +import _isEmpty from "lodash/isEmpty"; +import { FunctionComponent } from "react"; +import { CONTAIN_VARIABLE_SYNTAX_REGEX } from "utils/constants/regex"; +import { + DATE_FORMAT, + DateAdapter, + formatDate, + getMomentStyleOffset, + getTimeZoneAbbreviation, + getTimeZoneNames, + guessUserTimeZone, + parse, +} from "utils/date"; + +const fixValueForName = (name: string) => { + const offset = getMomentStyleOffset(name); + return `GMT${offset}`; +}; + +const getTimeZoneLabel = (name: string) => { + const offset = getMomentStyleOffset(name); + const gmtString = offset === "+00:00" ? "GMT" : `GMT ${offset}`; + const abbr = getTimeZoneAbbreviation(name); + return `(${gmtString}) ${abbr} (${name})`; +}; + +const TIME_ZONES_OPTIONS = getTimeZoneNames().map((name) => ({ + label: getTimeZoneLabel(name), + value: fixValueForName(name), +})); + +const defaultTimeZone = () => { + return fixValueForName(guessUserTimeZone()); +}; + +const extractDateStringFromValue = (val?: string) => { + if (!val || (val && CONTAIN_VARIABLE_SYNTAX_REGEX.test(val))) { + return [null, ""]; + } + + const splittedVal = val.split(" "); + + if (splittedVal!.length >= 1 && splittedVal!.length < 3) { + return [`${splittedVal[0]} 00:00`, defaultTimeZone()]; + } + + // If it's greater than 3 I don't care because it's wrong + const [dateField, hourField, timeZone] = splittedVal; + + return [`${dateField} ${hourField}`, timeZone]; +}; + +const UntilWaitTaskForm: FunctionComponent<{ + value: string; + onChange: (val: string) => void; +}> = ({ value, onChange }) => { + const [datetime, timeZone] = extractDateStringFromValue(value); + + return ( + + + + { + const validDateFormat = val ? formatDate(val, DATE_FORMAT) : ""; + + if ( + !_isEmpty(validDateFormat) && + !validDateFormat.includes("Invalid date") + ) { + onChange(`${validDateFormat} ${timeZone}`); + } else { + onChange(""); + } + }} + inputProps={{ fullWidth: true }} + /> + + + + { + if (selectedVal && !_isEmpty(datetime)) { + onChange(`${datetime} ${selectedVal.value}`); + } + }} + onInputChange={(__, typed: string) => { + if (!_isEmpty(typed) && !_isEmpty(datetime)) { + onChange(`${datetime} ${typed}`); + } + }} + renderOption={(props, option) => ( +
  • {option?.label}
  • + )} + /> +
    + + + + Computed value:  + + (can also be a   + + variable + + ) + + + } + InputLabelProps={{ + sx: { + pointerEvents: "auto", + }, + }} + value={value} + onChange={onChange} + /> + +
    +
    + ); +}; + +export default UntilWaitTaskForm; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/WaitTaskForm/WaitTaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/WaitTaskForm/WaitTaskForm.tsx new file mode 100644 index 0000000000..49d1a23190 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/WaitTaskForm/WaitTaskForm.tsx @@ -0,0 +1,194 @@ +import { Box, Link } from "@mui/material"; +import _omit from "lodash/omit"; +import { FunctionComponent, useMemo } from "react"; + +import MuiTypography from "components/MuiTypography"; +import { ConductorFlatMapFormBase } from "components/v1/FlatMapForm/ConductorFlatMapForm"; +import { WaitTaskDef } from "types"; +import { Optional } from "../OptionalFieldForm"; +import TaskFormSection from "../TaskFormSection"; +import { useGetSetHandler } from "../useGetSetHandler"; +import DurationWaitTaskForm from "./DurationWaitTaskForm"; +import SelectWaitType from "./SelectWaitType"; +import UntilWaitTaskForm from "./UntilWaitTaskForm"; +import { detectWaitType } from "./helpers"; +import { WaitTaskFormProps, WaitType } from "./types"; + +const inputParametersPath = "inputParameters"; + +const updateDuration = (task: WaitTaskDef, val: string) => ({ + ...task, + inputParameters: { + ..._omit(task.inputParameters, ["until"]), + duration: val, + }, +}); + +const updateUntil = (task: WaitTaskDef, val: string) => ({ + ...task, + inputParameters: { + ..._omit(task.inputParameters, ["duration"]), + until: val, + }, +}); + +const renderWaitTypeComponent = ({ + waitType, + task, + handler, +}: { + waitType: string; + task: WaitTaskDef; + handler: (val: any) => void; +}) => { + switch (waitType) { + case "duration": + return ( + + handler(updateDuration(task, val))} + /> + + ); + case "until": + return ( + + handler(updateUntil(task, val))} + /> + + ); + default: + return null; + } +}; + +export const WaitTaskForm: FunctionComponent = (props) => { + const { task, onChange } = props; + const [inputParametersValue, setInputParameters] = useGetSetHandler( + props, + inputParametersPath, + ); + const waitType = useMemo(() => detectWaitType(task), [task]); + + const handleChangeConfiguration = (val: WaitType) => { + switch (val) { + case WaitType.DURATION: + onChange(updateDuration(task, "1 days")); + break; + case WaitType.UNTIL: + onChange(updateUntil(task, "")); + break; + default: + onChange({ + ...task, + inputParameters: _omit(task.inputParameters, ["duration", "until"]), + }); + break; + } + }; + + return ( + + + + + + + {waitType === WaitType.UNTIL && ( + + Used to  + + wait until + +  a specified date & time, including the  + + timezone + + . + + )} + + {waitType === WaitType.DURATION && ( + + Specifies the  + + wait duration + +  in the format: x  + + hours + +  x  + + days + +  x  + + minutes + +  x  + + seconds + + + )} + + {waitType === WaitType.SIGNAL && ( + <> + + Used to wait for an external  + + signal. + + + + )} + + + {renderWaitTypeComponent({ + waitType, + task, + handler: onChange, + })} + + + + + + + + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/WaitTaskForm/helpers.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/WaitTaskForm/helpers.ts new file mode 100644 index 0000000000..9ae502f0ba --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/WaitTaskForm/helpers.ts @@ -0,0 +1,64 @@ +import _isString from "lodash/isString"; +import _isNil from "lodash/isNil"; +import { WaitTaskDef } from "types"; + +const coerceToFullWordNotation = (el: string): string => { + switch (el.trim()) { + case "days": + case "d": + return "days"; + case "hours": + case "hrs": + case "h": + return "hours"; + case "minutes": + case "mins": + case "m": + return "minutes"; + case "seconds": + case "secs": + case "s": + return "seconds"; + } + return el.trim(); +}; + +const defaultDurations: Array<[string, string]> = [ + ["", "days"], + ["", "hours"], + ["", "minutes"], + ["", "seconds"], +]; + +export function durationStringToPairs( + duration: string, +): Array<[string, string]> { + if (_isString(duration)) { + const durationArray = duration.split(/\s+/); + + if (duration.length > 0 && durationArray.length % 2 === 0) { + return defaultDurations.map(([value, unit]) => { + const validValueIndex = durationArray.findIndex( + (v) => coerceToFullWordNotation(v) === unit, + ); + const validValue = + validValueIndex > 0 ? durationArray[validValueIndex - 1] : value; + + return [validValue, unit]; + }); + } + + return defaultDurations; + } + + return defaultDurations; +} + +export const detectWaitType = (task: WaitTaskDef) => { + if (!_isNil(task?.inputParameters?.until)) { + return "until"; + } else if (!_isNil(task?.inputParameters?.duration)) { + return "duration"; + } + return "signal"; +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/WaitTaskForm/index.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/WaitTaskForm/index.ts new file mode 100644 index 0000000000..8ae8c153d7 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/WaitTaskForm/index.ts @@ -0,0 +1 @@ +export * from "./WaitTaskForm"; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/WaitTaskForm/types.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/WaitTaskForm/types.ts new file mode 100644 index 0000000000..c621b07a06 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/WaitTaskForm/types.ts @@ -0,0 +1,12 @@ +import { TaskFormProps } from "../types"; +import { WaitTaskDef } from "types/TaskType"; + +export interface WaitTaskFormProps extends TaskFormProps { + task: WaitTaskDef; +} + +export enum WaitType { + UNTIL = "until", + DURATION = "duration", + SIGNAL = "signal", +} diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/YieldTaskForm.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/YieldTaskForm.tsx new file mode 100644 index 0000000000..80acec1925 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/YieldTaskForm.tsx @@ -0,0 +1,61 @@ +import { useState } from "react"; +import { Box, Grid } from "@mui/material"; +import { path as _path } from "lodash/fp"; + +import { ConductorFlatMapFormBase } from "components/v1/FlatMapForm/ConductorFlatMapForm"; +import { ConductorCacheOutput } from "./ConductorCacheOutputForm"; + +import { SnackbarMessage } from "components/SnackbarMessage"; + +import { SchemaForm } from "./SchemaForm"; +import { TaskFormProps } from "./types"; +import TaskFormSection from "./TaskFormSection"; +import { updateField } from "utils/fieldHelpers"; +import { Optional } from "./OptionalFieldForm"; +import { useSchemaFormHandler } from "./hooks/useSchemaFormHandler"; + +const inputParametersPath = "inputParameters"; + +export const YieldTaskForm = ({ task, onChange }: TaskFormProps) => { + const [showAlert, setShowAlert] = useState(false); + const handleSchemaChange = useSchemaFormHandler({ task, onChange }); + + return ( + + {showAlert && ( + setShowAlert(false)} + anchorOrigin={{ horizontal: "right", vertical: "bottom" }} + /> + )} + + + + + onChange(updateField(inputParametersPath, value, task)) + } + /> + + + + + + + + + + + + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/editorConfig.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/editorConfig.ts new file mode 100644 index 0000000000..496f41df1f --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/editorConfig.ts @@ -0,0 +1,24 @@ +import { editor, type EditorOptions } from "shared/editor"; + +export const smallEditorDefaultOptions: EditorOptions = { + tabSize: 2, + minimap: { enabled: false }, + lightbulb: { enabled: editor.ShowLightbulbIconMode.Off }, + quickSuggestions: true, + lineNumbers: "off", + glyphMargin: false, + folding: false, + // Undocumented see https://github.com/Microsoft/vscode/issues/30795#issuecomment-410998882 + lineDecorationsWidth: 0, + lineNumbersMinChars: 0, + renderLineHighlight: "none", + overviewRulerLanes: 0, + hideCursorInOverviewRuler: true, + scrollbar: { + vertical: "hidden", + // this property is added because it was not allowing us to scroll when mouse pointer is over this component + alwaysConsumeMouseWheel: false, + }, + overviewRulerBorder: false, + automaticLayout: true, +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/hooks/useSchemaFormHandler.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/hooks/useSchemaFormHandler.ts new file mode 100644 index 0000000000..9893da1ba9 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/hooks/useSchemaFormHandler.ts @@ -0,0 +1,110 @@ +import { useCallback } from "react"; +import { assoc as _assoc, pipe as _pipe } from "lodash/fp"; +import { getAuthHeaders } from "shared/auth/tokenManagerJotai"; +import { getInputParametersFromSchemaIfNeeded } from "../../helpers"; +import { SchemaFormPropsValue } from "../SchemaForm"; +import { TaskFormProps } from "../types"; + +/** + * Checks if two values have the same type, handling special cases like arrays and null + */ +const typesMatch = (existingValue: unknown, defaultValue: unknown): boolean => { + const existingType = typeof existingValue; + const defaultType = typeof defaultValue; + + // Handle null separately (typeof null is "object" in JavaScript) + if (existingValue === null && defaultValue === null) return true; + if (existingValue === null || defaultValue === null) return false; + + // Handle arrays separately (typeof array is "object" in JavaScript) + const existingIsArray = Array.isArray(existingValue); + const defaultIsArray = Array.isArray(defaultValue); + if (existingIsArray && defaultIsArray) return true; + if (existingIsArray || defaultIsArray) return false; + + // For primitive types, compare directly + if (existingType !== defaultType) return false; + + // Both are objects (but not arrays or null) + if (existingType === "object") { + // For objects, we consider them matching if both are objects + // (we don't do deep comparison of object structure) + return true; + } + + return true; +}; + +/** + * Custom hook that handles schema form changes and automatically populates + * inputParameters from schema defaults when appropriate. + * + * @param props - TaskFormProps containing task and onChange + * @returns A handler function for SchemaForm onChange events + */ +export const useSchemaFormHandler = ({ task, onChange }: TaskFormProps) => { + const handleSchemaChange = useCallback( + async (schema?: SchemaFormPropsValue) => { + const updatedTask = _pipe( + _assoc("taskDefinition.inputSchema", schema?.inputSchema), + _assoc("taskDefinition.outputSchema", schema?.outputSchema), + _assoc("taskDefinition.enforceSchema", schema?.enforceSchema), + )(task); + + const authHeaders = getAuthHeaders(); + const defaultValues = await getInputParametersFromSchemaIfNeeded( + schema, + task, + authHeaders, + ); + + if (defaultValues) { + const existingParams = updatedTask.inputParameters || {}; + const mergedParams: Record = { ...defaultValues }; + + // Preserve existing parameters that have valid values and matching types + for (const [key, value] of Object.entries(existingParams)) { + const defaultValue = defaultValues[key]; + const valueType = typeof value; + + // If parameter exists in schema defaults, check type compatibility + if (defaultValue !== undefined) { + // If types don't match, use default value (don't preserve existing) + if (!typesMatch(value, defaultValue)) { + // Type mismatch - use default value (already in mergedParams) + continue; + } + } + + // Check if value is valid (should be kept) + if (valueType === "number") { + // For numbers, keep if not 0 + if (value !== 0) { + mergedParams[key] = value; + } + } else if (valueType === "boolean") { + // For booleans, always keep (we can't determine intent) + mergedParams[key] = value; + } else if (valueType === "string") { + // For strings, keep if not blank + const stringValue = value as string; + if (stringValue !== "" && stringValue?.trim() !== "") { + mergedParams[key] = value; + } + } else if (value != null) { + // For other types (objects, arrays, etc.), keep if not null/undefined + mergedParams[key] = value; + } + // If value is null, undefined, empty string, or 0, it's removed (not added to mergedParams) + } + + updatedTask.inputParameters = mergedParams; + } + + onChange(updatedTask); + }, + [task, onChange], + ); + + return handleSchemaChange; +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/hooks/useTaskForm.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/hooks/useTaskForm.ts new file mode 100644 index 0000000000..6dff3d490a --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/hooks/useTaskForm.ts @@ -0,0 +1,15 @@ +import _path from "lodash/fp/path"; + +import { updateField } from "utils/fieldHelpers"; +import { TaskFormProps } from "../types"; + +export const useTaskForm = ( + path: string, + { task, onChange }: TaskFormProps, +) => { + const value = _path(path, task); + + const setValue = (v: any) => onChange(updateField(path, v, task)); + + return [value, setValue]; +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/index.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/index.ts new file mode 100644 index 0000000000..eca981e85f --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/index.ts @@ -0,0 +1,39 @@ +export { DynamicForkForm as DynamicOperatorForm } from "./DynamicOperatorForm"; +export * from "./BusinessRuleForm"; +export * from "./SendgridForm"; +export * from "./DoWhileTaskForm"; +export * from "./DynamicForkOperatorForm"; +export * from "./EventTaskForm"; +export * from "./GetDocumentTaskForm"; +export * from "./GetWorkflowTaskForm"; +export * from "./HTTPTaskForm"; +// HumanTaskForm moved to enterprise/plugins/human-tasks/components/HumanTaskForm +export * from "./KafkaTaskForm"; +export * from "./INLINETaskForm"; +export * from "./JDBCTaskForm"; +export * from "./JOINTaskForm"; +export * from "./JSONJQTransformForm"; +export * from "./LLMChatCompleteTaskForm"; +export * from "./LLMGenerateEmbeddingsTaskForm"; +export * from "./LLMGetEmbeddingsTaskForm"; +export * from "./LLMIndexDocumentTaskForm"; +export * from "./LLMIndexTextTaskForm"; +export * from "./LLMSearchIndexTaskForm"; +export * from "./LLMStoreEmbeddingsTaskForm"; +export * from "./LLMTextCompleteTaskForm"; +export * from "./OpsGenieTaskForm"; +export * from "./ParseDocumentTaskForm"; +export * from "./QueryProcessorTaskForm"; +export * from "./SetVariableOperatorForm"; +export * from "./SimpleTaskForm"; +export * from "./StartWorkflowTaskForm"; +export * from "./SubWorkflowOperatorForm"; +export * from "./SwitchTaskForm"; +export * from "./TerminateOperatorForm"; +export * from "./TerminateWorkflowForm"; +export * from "./UnknownTaskForm"; +export * from "./WaitTaskForm"; +export * from "./YieldTaskForm"; +export * from "./ListFilesTaskForm"; +export { default as GetSignedJwtForm } from "./GetSignedJwtForm"; +export * from "./ChunkTextTaskForm"; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/maybeVariableHOC.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/maybeVariableHOC.tsx new file mode 100644 index 0000000000..4ac89410a1 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/maybeVariableHOC.tsx @@ -0,0 +1,41 @@ +import { FormTaskType } from "types/TaskType"; +import { MaybeVariable } from "./MaybeVariable"; +import { FunctionComponent } from "react"; + +type CommonProps = { + label?: string; + taskType: FormTaskType; + path: string; + onChange?: (val: any) => void; + value?: any; + onChangeHeaders?: (headers: any) => void; +}; + +function maybeVariable( + WrappedComponent: FunctionComponent, +): FunctionComponent { + return function WrapperComponent(props: T & CommonProps) { + const handleChange = (e: string) => { + if (props.onChange) { + return props.onChange(e); + } else if (props.onChangeHeaders) { + return props.onChangeHeaders(e); + } else { + return () => {}; + } + }; + if (props.taskType && props.path) { + return ( + + + + ); + } else return null; + }; +} +export default maybeVariable; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/types.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/types.ts new file mode 100644 index 0000000000..66b83b6287 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/types.ts @@ -0,0 +1,14 @@ +import { TaskDef } from "types"; +import { ActorRef } from "xstate"; +import { TaskHeaderMachineEvents } from "./TaskFormHeader/state"; + +export interface TaskFormProps { + task: Partial; + onChange: any; + updateAdditionalFieldMetadata?: any; + additionalFieldMetadata?: any; + isMetaBarEditing?: boolean; + onToggleExpand?: any; + collapseWorkflowList?: string[]; + taskFormHeaderActor?: ActorRef; +} diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/useGetSetHandler.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/useGetSetHandler.ts new file mode 100644 index 0000000000..99621fed9e --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/forms/useGetSetHandler.ts @@ -0,0 +1,13 @@ +import _get from "lodash/get"; +import _clone from "lodash/clone"; +import { updateField } from "utils/fieldHelpers"; +import { TaskFormProps } from "./types"; + +export const useGetSetHandler = ( + { task, onChange }: TaskFormProps, + path: string, +) => + [ + _clone(_get(task, path)), + (val: any) => onChange(updateField(path, val, task)), + ] as const; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/helpers.test.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/helpers.test.ts new file mode 100644 index 0000000000..f45633b380 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/helpers.test.ts @@ -0,0 +1,190 @@ +import { TaskDef, TaskType } from "types/common"; +import { + getCorrespondingJoinTask, + updateInputParametersCommon, +} from "./helpers"; + +const taskJson = { + name: "start_workflow", + taskReferenceName: "start_workflow_ref", + inputParameters: { + startWorkflow: { + name: "SUB_WORKFLOW_TASK_TEST_WF", + input: {}, + version: 1, + }, + }, + type: TaskType.START_WORKFLOW, +}; + +const task = { + name: "start_workflow", + taskReferenceName: "start_workflow_ref", + inputParameters: { + startWorkflow: { + name: "", + input: {}, + }, + }, + type: TaskType.START_WORKFLOW, +}; + +const getWorkflowDefinitionByNameAndVersionFn: any = (_params: any) => { + return { + createTime: 1709828406534, + updateTime: 1709828406538, + name: "SUB_WORKFLOW_TASK_TEST_WF", + description: "donot delete this workflow. used for tests.", + version: 1, + tasks: [ + { + name: "http", + taskReferenceName: "http_ref", + inputParameters: { + uri: "https://orkes-api-tester.orkesconductor.com/api", + method: "GET", + connectionTimeOut: 3000, + readTimeOut: "3000", + accept: "application/json", + contentType: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + inputParameters: ["Name", "Age", "Address"], + outputParameters: {}, + failureWorkflow: "", + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: false, + ownerEmail: "najeeb.thangal@orkes.io", + timeoutPolicy: "ALERT_ONLY", + timeoutSeconds: 0, + variables: {}, + inputTemplate: {}, + }; +}; + +const expectedResult = { + name: "start_workflow", + taskReferenceName: "start_workflow_ref", + inputParameters: { + startWorkflow: { + name: "SUB_WORKFLOW_TASK_TEST_WF", + input: { + Name: "", + Age: "", + Address: "", + }, + version: 1, + }, + }, + type: "START_WORKFLOW", +}; + +describe("updateInputParametersCommon", () => { + it("return expected result", () => { + const onChangePromise = new Promise((resolve) => { + const onChange = (data: Partial) => { + resolve({ ...data }); + }; + + updateInputParametersCommon( + taskJson, + task, + {}, + onChange, + "inputParameters.startWorkflow", + "inputParameters.startWorkflow.input", + TaskType.START_WORKFLOW, + getWorkflowDefinitionByNameAndVersionFn, + ); + }); + + return onChangePromise.then((result: any) => { + expect(result).toEqual(expectedResult); + }); + }); +}); + +describe("getCorrespondingJoinTask", () => { + it("return corresponding join task of the fork", () => { + const originalTask = { + taskReferenceName: "fork_join", + type: TaskType.FORK_JOIN, + }; + const tasksList = [ + { taskReferenceName: "http", type: TaskType.HTTP }, + { taskReferenceName: "fork_join", type: TaskType.FORK_JOIN }, + { taskReferenceName: "join", type: TaskType.JOIN }, + ]; + const expectedResult = [{ taskReferenceName: "join", type: TaskType.JOIN }]; + const correspondingJoinTask = getCorrespondingJoinTask( + originalTask, + tasksList, + ); + expect(correspondingJoinTask).toEqual(expectedResult); + }); + it("return empty array if corresponding join task of the fork not found", () => { + const originalTask = { + taskReferenceName: "fork_join_1", + type: TaskType.FORK_JOIN, + }; + const tasksList = [ + { taskReferenceName: "http", type: TaskType.HTTP }, + { taskReferenceName: "fork_join", type: TaskType.FORK_JOIN }, + { taskReferenceName: "join", type: TaskType.JOIN }, + ]; + const correspondingJoinTask = getCorrespondingJoinTask( + originalTask, + tasksList, + ); + expect(correspondingJoinTask).toEqual([]); + }); + it("return corresponding join task of the fork - multiple fork joins are present", () => { + const originalTask = { + taskReferenceName: "fork_join_2", + type: TaskType.FORK_JOIN, + }; + const tasksList = [ + { taskReferenceName: "http", type: TaskType.HTTP }, + { taskReferenceName: "fork_join", type: TaskType.FORK_JOIN }, + { taskReferenceName: "join", type: TaskType.JOIN }, + { taskReferenceName: "http_3", type: TaskType.HTTP }, + { taskReferenceName: "http_1", type: TaskType.HTTP }, + { taskReferenceName: "fork_join_2", type: TaskType.FORK_JOIN }, + { taskReferenceName: "join_2", type: TaskType.JOIN }, + { taskReferenceName: "http_3", type: TaskType.HTTP }, + ]; + const expectedResult = [ + { taskReferenceName: "join_2", type: TaskType.JOIN }, + ]; + const correspondingJoinTask = getCorrespondingJoinTask( + originalTask, + tasksList, + ); + expect(correspondingJoinTask).toEqual(expectedResult); + }); + it("return empty array if tasksList is undefined", () => { + const originalTask = { + taskReferenceName: "fork_join", + type: TaskType.FORK_JOIN, + }; + const tasksList = undefined; + const correspondingJoinTask = getCorrespondingJoinTask( + originalTask, + tasksList, + ); + expect(correspondingJoinTask).toEqual([]); + }); +}); diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/helpers.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/helpers.ts new file mode 100644 index 0000000000..b415c15988 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/helpers.ts @@ -0,0 +1,341 @@ +import { Monaco } from "@monaco-editor/react"; +import _path from "lodash/fp/path"; +import _update from "lodash/fp/update"; +import _keys from "lodash/keys"; +import _nth from "lodash/nth"; +import { IdempotencyValuesProp } from "pages/definition/RunWorkflow/state"; +import { IdempotencyStrategyEnum } from "pages/runWorkflow/types"; +import { MutableRefObject } from "react"; +import { + DoWhileTaskDef, + InlineTaskDef, + JDBCTaskDef, + SwitchTaskDef, +} from "types/TaskType"; +import { AuthHeaders, TaskDef, TaskType } from "types/common"; +import { logger } from "utils/logger"; +import { mock } from "mock-json-schema"; +import { queryClient } from "queryClient"; +import { fetchWithContext, fetchContextNonHook } from "plugins/fetch"; +import { JsonSchema } from "@jsonforms/core"; + +const VARIABLE_DEFINER = "$."; +const IDLE_MINIMUM_VALUE_IF_FAIL_TO_GET_REF = 500; +const A_MARGIN_THREASHHOLD = 22; + +export type OnlyTheWordInfoProp = { + word: string; + startColumn: number; + endColumn: number; +}; + +export const editorAddCommandAltEnter = ( + editor: Monaco, + monaco: Monaco, + taskRef: MutableRefObject< + | Partial + | Partial + | Partial + | Partial + | null + >, + callBack: (onlyTheWordInfo: OnlyTheWordInfoProp) => void, +) => { + return editor.addCommand(monaco.KeyMod.Alt | monaco.KeyCode.Enter, () => { + const position = editor.getPosition(); // Get the current cursor position + const model = editor.getModel(); + + if (model) { + const onlyTheWordInfo: OnlyTheWordInfoProp = + model.getWordAtPosition(position); // This only selects the word + + const startColumn = onlyTheWordInfo?.startColumn; + if (startColumn > VARIABLE_DEFINER.length) { + // Avoid blowing up because of wrong position. + const newStart = Math.max(startColumn - VARIABLE_DEFINER.length, 1); // We select a new start + let word = null; + // Create a new range from th new start including $. + const wordRange = new monaco.Range( + position.lineNumber, + newStart, + position.lineNumber, + onlyTheWordInfo.endColumn, + ); + word = model.getValueInRange(wordRange); + if (word && word?.includes(VARIABLE_DEFINER)) { + const maybeNewVariable = word.word; + const currentVariables = _keys( + taskRef.current?.inputParameters || {}, + ); + + if (!currentVariables.includes(maybeNewVariable)) { + callBack(onlyTheWordInfo); + } + } + } + } + }); +}; + +export const editorHandleAutoSize = ( + editor: Monaco, + parentWrapperRef: MutableRefObject, +) => { + //auto scrolling according to the content height => https://github.com/microsoft/monaco-editor/issues/794#issuecomment-688959283 + const updateHeight = () => { + const contentHeight = Math.min(1000, editor.getContentHeight()); + let contentWidth = IDLE_MINIMUM_VALUE_IF_FAIL_TO_GET_REF; + + if (parentWrapperRef) { + contentWidth = parentWrapperRef.current.getBoundingClientRect().width; + } + try { + editor.layout({ + width: contentWidth - A_MARGIN_THREASHHOLD, + height: contentHeight, + }); + } catch { + /* empty */ + } + }; + editor.onDidContentSizeChange(updateHeight); + updateHeight(); +}; + +export const editorDecorations = ( + model: Monaco, + parameters: string[], + monaco: Monaco, +) => { + return parameters.map((word: string) => { + const escapedWord = word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const wordRegex = new RegExp( + `${escapedWord}(?![a-zA-Z0-9_$])(?:\\.\\w+|\\['\\w+'\\]|\\[\\w+\\])*`, + "g", + ); + let match; + const decorators = []; + while ((match = wordRegex.exec(model.getValue()))) { + const startPos = model.getPositionAt(match.index); + const endPos = model.getPositionAt(match.index + match[0].length); + + if (startPos && endPos) { + decorators.push({ + range: new monaco.Range( + startPos.lineNumber, + startPos.column, + endPos.lineNumber, + endPos.column, + ), + options: { + className: "squiggly-error", + }, + }); + } + } + return decorators; + }); +}; + +export const updateInputParametersCommon = async ( + taskJson: Partial, + originalTask: Partial, + authHeaders: AuthHeaders, + onChange: (data: Partial) => void, + workflowNameVersionStringPath: string, + inputParametersStringPath: string, + taskType: TaskType.START_WORKFLOW | TaskType.SUB_WORKFLOW, + getWorkflowDefinitionByNameAndVersionFn: ({ + name, + version, + authHeaders, + }: { + name: string; + version: number; + authHeaders: AuthHeaders; + }) => Promise, +) => { + const wfName = _path(`${workflowNameVersionStringPath}.name`, taskJson) || ""; + const wfVersion = + _path(`${workflowNameVersionStringPath}.version`, taskJson) || ""; + + if ( + (wfName !== + (_path(`${workflowNameVersionStringPath}.name`, originalTask) || "") || + wfVersion !== + (_path(`${workflowNameVersionStringPath}.version`, originalTask) || + "")) && + [wfName, wfVersion].every( + (val) => + val != null && String(val).trim() !== "" && !String(val).includes("$"), + ) + ) { + try { + const workflowDef = await getWorkflowDefinitionByNameAndVersionFn({ + name: wfName, + version: wfVersion as number, + authHeaders, + }); + const entries = workflowDef?.inputParameters.map((value: string) => [ + value, + _path(`${inputParametersStringPath}.${[value]}`, taskJson) || "", + ]); + + if (entries && entries.length > 0) { + const inputParams = Object.fromEntries(entries); + + const payloadForStartWorkflow = { + ...taskJson.inputParameters, + startWorkflow: { + ...taskJson.inputParameters?.startWorkflow, + input: inputParams, + }, + }; + const payload = + taskType === TaskType.START_WORKFLOW + ? payloadForStartWorkflow + : inputParams; + + onChange({ + ...taskJson, + inputParameters: payload, + }); + } else { + onChange({ ...taskJson }); + } + } catch (error) { + logger.error(error); + return; + } + } else { + onChange({ ...taskJson }); + } +}; + +export const handleChangeIdempotencyValues = ( + data: IdempotencyValuesProp, + task: Partial, + path: string, + onChange: (task: Partial) => void, +) => { + const idempotencyStrategy = () => { + if (!data?.idempotencyKey && task.type !== TaskType.SUB_WORKFLOW) { + return undefined; + } + if (data.idempotencyStrategy) { + return data.idempotencyStrategy; + } + if (_path(`${path}.idempotencyStrategy`, task)) { + return _path(`${path}.idempotencyStrategy`, task); + } + return IdempotencyStrategyEnum.RETURN_EXISTING; + }; + + const maybeIdempotencyStrategy = idempotencyStrategy(); + const maybeIdempotencyKey = + data?.idempotencyKey === "" ? undefined : data?.idempotencyKey; + + const taskJson = { ...task }; + + const updatedTask = _update( + path, + (item) => ({ + ...item, + idempotencyKey: maybeIdempotencyKey, + idempotencyStrategy: maybeIdempotencyStrategy, + }), + taskJson, + ); + + onChange({ ...updatedTask }); +}; + +export const getCorrespondingJoinTask = ( + originalTask: Partial, + tasksList: Partial[] = [], +) => { + if (originalTask && tasksList && tasksList.length > 0) { + const taskIndex = tasksList.findIndex( + (item) => item?.taskReferenceName === originalTask?.taskReferenceName, + ); + if (taskIndex > -1) { + const nextTask = _nth(tasksList, taskIndex + 1); + if (nextTask && nextTask.type === TaskType.JOIN) { + return [nextTask]; + } + return []; + } + return []; + } + return []; +}; + +const fetchContext = fetchContextNonHook(); + +/** + * Fetches a schema by name and version, then generates default values from it + * @param schemaName - The name of the schema + * @param schemaVersion - The version of the schema (optional) + * @param authHeaders - Authentication headers for the API request + * @returns Promise that resolves to default values object, or null if fetching/generation fails + */ +export const getDefaultValuesFromSchema = async ( + schemaName: string, + schemaVersion: number | undefined, + authHeaders: AuthHeaders, +): Promise | null> => { + if (!schemaName) { + return null; + } + + try { + const url = `/schema/${schemaName}${schemaVersion ? `/${schemaVersion}` : ""}`; + + const response = await queryClient.fetchQuery( + [fetchContext.stack, url], + () => fetchWithContext(url, fetchContext, { headers: authHeaders }), + ); + + if (response?.data) { + const defaultValues = mock(response.data as JsonSchema); + if (defaultValues && Object.keys(defaultValues).length > 0) { + return defaultValues as Record; + } + } + return null; + } catch (error) { + logger.warn("Failed to fetch schema for default values:", error); + return null; + } +}; + +/** + * Checks if inputParameters should be populated from schema and returns default values if conditions are met + * @param newSchema - The new schema form value + * @param currentTask - The current task definition + * @param authHeaders - Authentication headers for the API request + * @returns Promise that resolves to default values object if conditions are met, or null otherwise + */ +export const getInputParametersFromSchemaIfNeeded = async ( + newSchema: { inputSchema?: { name?: string; version?: number } } | undefined, + currentTask: Partial | undefined, + authHeaders: AuthHeaders, +): Promise | null> => { + // Check if inputSchema is being updated and inputParameters is empty + const hasInputSchema = newSchema?.inputSchema?.name; + const inputSchemaChanged = + newSchema?.inputSchema?.name !== + currentTask?.taskDefinition?.inputSchema?.name || + newSchema?.inputSchema?.version !== + currentTask?.taskDefinition?.inputSchema?.version; + + if (hasInputSchema && inputSchemaChanged && newSchema?.inputSchema?.name) { + return await getDefaultValuesFromSchema( + newSchema.inputSchema.name, + newSchema.inputSchema.version, + authHeaders, + ); + } + + return null; +}; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/index.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/index.ts new file mode 100644 index 0000000000..3a5aa3ed48 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/index.ts @@ -0,0 +1,2 @@ +import TaskForm from "./TaskForm"; +export { TaskForm }; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/state/TaskFormContext/TaskFormContext.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/state/TaskFormContext/TaskFormContext.tsx new file mode 100644 index 0000000000..04a240b5be --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/state/TaskFormContext/TaskFormContext.tsx @@ -0,0 +1,6 @@ +import { createContext } from "react"; +import { TaskFormContextProviderProps } from "./types"; + +export const TaskFormContext = createContext({ + formTaskActor: undefined, +}); diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/state/TaskFormContext/TaskFormProvider.tsx b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/state/TaskFormContext/TaskFormProvider.tsx new file mode 100644 index 0000000000..c6326676dd --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/state/TaskFormContext/TaskFormProvider.tsx @@ -0,0 +1,11 @@ +import { FunctionComponent } from "react"; +import { TaskFormContext } from "./TaskFormContext"; +import { TaskFormContextProviderProps } from "./types"; + +export const TaskFormContextProvider: FunctionComponent< + TaskFormContextProviderProps +> = ({ children, formTaskActor }) => ( + + {children} + +); diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/state/TaskFormContext/index.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/state/TaskFormContext/index.ts new file mode 100644 index 0000000000..d7980d6f6f --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/state/TaskFormContext/index.ts @@ -0,0 +1,2 @@ +export * from "./TaskFormContext"; +export * from "./TaskFormProvider"; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/state/TaskFormContext/types.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/state/TaskFormContext/types.ts new file mode 100644 index 0000000000..5aa2a93ef1 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/state/TaskFormContext/types.ts @@ -0,0 +1,8 @@ +import { ReactNode } from "react"; +import { ActorRef } from "xstate"; +import { TaskFormEvents } from "../types"; + +export interface TaskFormContextProviderProps { + formTaskActor?: ActorRef; + children?: ReactNode; +} diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/state/actions.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/state/actions.ts new file mode 100644 index 0000000000..970e1a86f4 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/state/actions.ts @@ -0,0 +1,136 @@ +import { + ActorRef, + assign, + pure, + send, + sendParent, + sendTo, + spawn, +} from "xstate"; +import { + UpdateTaskEvent, + TaskFormMachineContext, + UpdateCrumbsEvent, + SelectEdgeEvent, +} from "./types"; +import { REPLACE_TASK_EVT } from "../../../state/constants"; +import { + TaskFormHeaderEventTypes, + taskFormHeaderMachine, + TaskHeaderMachineEvents, +} from "pages/definition/EditorPanel/TaskFormTab/forms/TaskFormHeader/state"; +import { TaskType, TaskDef } from "types"; +import { TaskStatsEventTypes } from "../TaskStats/state"; +import _isNil from "lodash/isNil"; +import fastDeepEqual from "fast-deep-equal"; +import { FlowActionTypes } from "components/flow/state"; +import _isUndefined from "lodash/isUndefined"; +import _omitBy from "lodash/omitBy"; + +const maybeUseChanges = ( + defaultTo?: Partial, + maybeTask?: Partial, +): Partial | undefined => { + if ( + _isNil(maybeTask) || + ![TaskType.SWITCH, TaskType.DO_WHILE].includes(maybeTask?.type as TaskType) + ) { + return defaultTo; + } + return fastDeepEqual(maybeTask, defaultTo) ? defaultTo : maybeTask; +}; + +export const spawnTaskHeaderMachineActor = assign( + (context) => ({ + taskHeaderActor: spawn( + taskFormHeaderMachine.withContext({ + name: context?.originalTask?.name || "", + taskReferenceName: context?.originalTask?.taskReferenceName || "", + taskType: context?.originalTask?.type || TaskType.SIMPLE, // TODO what if taskType is not set + }), + "taskFormHeader-fields", + ), + }), +); + +export const updateTask = assign({ + taskChanges: ({ taskChanges }, event) => { + return _omitBy({ ...taskChanges, ...event.taskChanges }, _isUndefined); + }, +}); + +export const updateCollapseWorkflowList = sendParent< + TaskFormMachineContext, + any +>((_, { workflowName }) => ({ + type: FlowActionTypes.UPDATE_COLLAPSE_WORKFLOW_LIST, + workflowName: workflowName, +})); + +export const updateCrumbsAndOriginalTask = assign< + TaskFormMachineContext, + UpdateCrumbsEvent +>({ + crumbs: (_context, event) => { + return event.crumbs; + }, + originalTask: (context, event) => + maybeUseChanges(context.taskChanges, event?.task), + taskChanges: (context, event) => + maybeUseChanges(context.taskChanges, event?.task), +}); + +export const maybePersistSelectedSwitchBranch = assign< + TaskFormMachineContext, + SelectEdgeEvent +>({ + maybeSelectedSwitchBranch: (context, { edge: { text } }) => text, +}); + +/* export const checkForErrors = sendParent( */ +/* ({ taskChanges }) => ({ */ +/* type: ErrorInspectorEventTypes.VALIDATE_SINGLE_TASK, */ +/* task: taskChanges, */ +/*}) */ +/* ); */ + +export const notifyChanges = sendParent( + ({ originalTask, crumbs, taskChanges }: TaskFormMachineContext) => ({ + type: REPLACE_TASK_EVT, + task: originalTask, + crumbs, + newTask: taskChanges, + }), +); + +export const notifyNameChange = send( + ({ originalTask }) => ({ + type: TaskStatsEventTypes.UPDATE_TASK_NAME, + name: originalTask?.name, + }), + { to: "taskStatsMachine" }, +); + +export const updateTaskHeaderMachine = pure( + ({ originalTask }, { taskChanges }) => { + if (taskChanges?.type !== originalTask?.type) { + return sendTo< + TaskFormMachineContext, + any, + ActorRef + >( + "taskFormHeader-fields", + ({ originalTask }, { taskChanges }) => { + return { + type: TaskFormHeaderEventTypes.VALUES_UPDATED, + name: taskChanges?.name || "", + taskReferenceName: taskChanges?.taskReferenceName || "", + taskType: + taskChanges?.type || originalTask?.type || TaskType.SIMPLE, + }; + }, + { delay: 50 }, + ); + } + }, +); diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/state/guards.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/state/guards.ts new file mode 100644 index 0000000000..2767d176b0 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/state/guards.ts @@ -0,0 +1,10 @@ +import fastDeepEqual from "fast-deep-equal"; +import { + TaskFormMachineContext, + UpdateTaskEvent, +} from "pages/definition/EditorPanel/TaskFormTab/state/types"; + +export const isTaskChanged = ( + context: TaskFormMachineContext, + event: UpdateTaskEvent, +) => !fastDeepEqual(context.taskChanges, event.taskChanges); diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/state/index.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/state/index.ts new file mode 100644 index 0000000000..f05e92a22c --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/state/index.ts @@ -0,0 +1,3 @@ +export * from "./machine"; +export * from "./TaskFormContext"; +export * from "./types"; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/state/machine.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/state/machine.ts new file mode 100644 index 0000000000..6543076f4e --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/state/machine.ts @@ -0,0 +1,77 @@ +import { createMachine } from "xstate"; +import { + FormMachineActionTypes, + TaskFormEvents, + TaskFormMachineContext, +} from "./types"; +import { FlowActionTypes } from "components/flow/state"; +import * as actions from "./actions"; +import * as guards from "./guards"; +import { taskStatsMachine } from "../TaskStats/state"; + +export const formMachine = createMachine< + TaskFormMachineContext, + TaskFormEvents +>( + { + id: "formMachine", + predictableActionArguments: true, + initial: "init", + context: { + originalTask: undefined, + crumbs: [], + tasksBranch: [], + workflowInputParameters: [], + taskHeaderActor: undefined, + maybeSelectedSwitchBranch: undefined, + authHeaders: undefined, + taskChanges: undefined, + }, + invoke: { + id: "taskStatsMachine", + src: taskStatsMachine, + data: { + completedRateSeries: [], + failedRateSeries: [], + completedAmount: 0, + failedAmount: 0, + startHoursBack: 24, + authHeaders: ({ authHeaders }: TaskFormMachineContext) => authHeaders, + taskName: ({ originalTask }: TaskFormMachineContext) => + originalTask?.name, + }, + }, + states: { + init: { + entry: ["spawnTaskHeaderMachineActor"], + always: "rendered", + }, + noTask: { + entry: "invalidTaskLeaveForm", + type: "final", + }, + rendered: { + on: { + [FormMachineActionTypes.UPDATE_TASK]: { + cond: "isTaskChanged", + actions: ["updateTask", "notifyChanges", "updateTaskHeaderMachine"], + }, + [FormMachineActionTypes.UPDATE_CRUMBS]: { + // Note it will use incoming task if task is SWITCH and has changes else it will ignore + actions: ["updateCrumbsAndOriginalTask"], + }, + [FlowActionTypes.SELECT_EDGE_EVT]: { + actions: ["maybePersistSelectedSwitchBranch"], + }, + [FlowActionTypes.UPDATE_COLLAPSE_WORKFLOW_LIST]: { + actions: ["updateCollapseWorkflowList"], + }, + }, + }, + }, + }, + { + actions: actions as any, + guards: guards as any, + }, +); diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/state/types.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/state/types.ts new file mode 100644 index 0000000000..7759766477 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/state/types.ts @@ -0,0 +1,66 @@ +import { TaskDef, AuthHeaders, Crumb } from "types"; +import { TaskHeaderMachineEvents } from "pages/definition/EditorPanel/TaskFormTab/forms/TaskFormHeader/state/types"; +import { ActorRef } from "xstate"; +import { EdgeData } from "reaflow"; +import { FlowActionTypes } from "components/flow/state"; + +export enum FormMachineActionTypes { + UPDATE_TASK = "UPDATE_TASK", + CHECK_FOR_TASK_ERRORS = "CHECK_FOR_TASK_ERRORS", + + UPDATE_CRUMBS = "UPDATE_CRUMBS", + + UPDATE_COLLAPSE_WORKFLOW_LIST = "UPDATE_COLLAPSE_WORKFLOW_LIST", +} + +export type ErrorType = { + id: "Form Error"; + message: string; + path: string; + hint?: string; + type: "TASK"; +}; + +export type UpdateTaskEvent = { + type: FormMachineActionTypes.UPDATE_TASK; + taskChanges: Partial; +}; + +export type UpdateCollapseWorkflowListEvent = { + type: FormMachineActionTypes.UPDATE_COLLAPSE_WORKFLOW_LIST; + workflowName: string; +}; + +export type UpdateCrumbsEvent = { + type: FormMachineActionTypes.UPDATE_CRUMBS; + crumbs: Crumb[]; + task?: Partial; +}; + +export type CheckForTaskErrorsEvent = { + type: FormMachineActionTypes.CHECK_FOR_TASK_ERRORS; +}; + +export type SelectEdgeEvent = { + type: FlowActionTypes.SELECT_EDGE_EVT; + edge: EdgeData; +}; + +export interface TaskFormMachineContext { + originalTask?: Partial; + taskChanges?: Partial; + tasksBranch: TaskDef[]; + crumbs: Crumb[]; + workflowInputParameters: string[]; + taskHeaderActor?: ActorRef; + maybeSelectedSwitchBranch?: string; + authHeaders?: AuthHeaders; + workflowName?: string; +} + +export type TaskFormEvents = + | UpdateTaskEvent + | CheckForTaskErrorsEvent + | SelectEdgeEvent + | UpdateCrumbsEvent + | UpdateCollapseWorkflowListEvent; diff --git a/ui-next/src/pages/definition/EditorPanel/TaskFormTab/taskDescription.ts b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/taskDescription.ts new file mode 100644 index 0000000000..af0e76e835 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/TaskFormTab/taskDescription.ts @@ -0,0 +1,88 @@ +import { FormTaskType } from "types/TaskType"; +import { TaskType } from "types/common"; + +type TaskDescriptions = Partial>; + +export const taskDescriptions: TaskDescriptions = { + // system + [TaskType.EVENT]: + "EVENT is a task used to publish an event into one of the supported eventing systems in Conductor.", + [TaskType.HTTP]: + "HTTP task allows you to make calls to remote services exposed over HTTP/HTTPS.", + [TaskType.HTTP_POLL]: + "The HTTP_POLL is a conductor task used to invoke HTTP API until the specified condition matches.", + [TaskType.JSON_JQ_TRANSFORM]: + "The JSON_JQ_TRANSFORM task is a System task that allows the processing of JSON data that is supplied to the task by using the popular JQ processing tool’s query expression language.", + [TaskType.INLINE]: + "The inline task helps execute necessary logic at the workflow run-time using an evaluator. The two supported evaluator types are javascript and graaljs.", + [TaskType.BUSINESS_RULE]: + "Business rule task helps evaluate business rules compiled in spreadsheets.", + [TaskType.SENDGRID]: "Send email using sendgrid", + [TaskType.START_WORKFLOW]: + "Start Workflow is an operator task used to start another workflow from an existing workflow. Unlike a sub-workflow task, a start workflow task doesn’t create a relationship between the current workflow and the newly started workflow. That means it doesn’t wait for the started workflow to get completed.", + [TaskType.WAIT_FOR_WEBHOOK]: + "Webhook is an HTTP-based callback function that facilitates the communication between the Conductor and other third-party systems. It can be used to receive data from other applications to the Conductor.", + [TaskType.UPDATE_SECRET]: + "A system task to update the value of any secret, given the user has permission to update the secret.", + [TaskType.QUERY_PROCESSOR]: + "A system task for executing queries across different systems, tailored for purposes like alert generation.", + [TaskType.UPDATE_TASK]: "A system task to update the status of other tasks.", + + // operator + + [TaskType.SWITCH]: + "The switch task is used for creating branching logic. It is a representation of multiple if...then...else or switch...case statements in programming.", + [TaskType.DO_WHILE]: + "The Do While task sequentially executes a list of tasks as long as a condition is true. The list of tasks is executed first before the condition is checked, even for the first iteration, just like a regular do .. while task in programming languages.", + [TaskType.FORK_JOIN_DYNAMIC]: + "A Fork/Join task can be used when you need to run tasks in parallel. It contains two components, the fork, and the join part. A fork operation lets you run a specified list of tasks in parallel. A fork task is followed by a join operation that waits on the forked tasks to finish. The JOIN task also collects outputs from each of the forked tasks.", + [TaskType.DYNAMIC]: + "The dynamic task allows us to execute one of the registered tasks dynamically at run-time. This means that you can run a task not fixed at the time of the workflow’s execution. The task name could even be supplied as part of the workflow’s input and be mapped to the dynamic task input.", + [TaskType.TERMINATE]: + "The Terminate task is a task that can terminate the current workflow with a termination status and reason.", + [TaskType.SET_VARIABLE]: + "Set Variable allows us to set the workflow variables by creating or updating them with new values. Think of these as a temporary state, which you can set in any step and refer back to any steps that execute after setting the value.", + [TaskType.SUB_WORKFLOW]: + "Sub Workflow allows executing another workflow from within the current workflow.", + [TaskType.JOIN]: + "A JOIN task is used in conjunction with a FORK_JOIN or FORK_JOIN_DYNAMIC task to join all the tasks within the forks.", + [TaskType.WAIT]: + "The Wait task is used when the workflow needs to be paused for an external signal to continue. It is used when the workflow needs to wait and pause for external signals, such as a human intervention (like manual approval) or an event coming from an external source, such as Kafka or SQS.", + [TaskType.TERMINATE_WORKFLOW]: + "The Terminate Workflow task is used to terminate other workflows using their workflow IDs.", + [TaskType.HUMAN]: + "Human tasks are used when you need to wait your workflow for an interaction with a human. When your workflow reaches the human task, it waits for a manual interaction to proceed with the workflow. It can be leveraged when you need manual approval from a human, such as when a form needs to be approved within an application, such as approval workflows.", + [TaskType.GET_WORKFLOW]: + "Get Workflow task is used to retrieve detail of workflow using workflow ID.", + + // worker + [TaskType.JDBC]: + "A JDBC task is a system task used to execute or store information in MySQL.", + [TaskType.SIMPLE]: + "A Simple task is a Worker task that requires an external worker for polling. The Workers can be implemented in any language, and Conductor SDKs provide additional features such as metrics, server communication, and polling threads that make the worker creation process easier.", + + // alerting + [TaskType.OPS_GENIE]: + "A system task to send alerts to Opsgenie in the event of workflow failures. This task can be used in conjunction with the Query Processor task, which fetches metadata details to trigger alerts to Opsgenie as required.", + + // ai/llm + + [TaskType.LLM_TEXT_COMPLETE]: + "A system task to predict or generate the next phrase or words in a given text based on the context provided.", + [TaskType.LLM_GENERATE_EMBEDDINGS]: + "A system task to generate embeddings from the input data provided. Embeddings are the processed input text converted into a sequence of vectors, which can then be stored in a vector database for retrieval later. You can use a model that was previously integrated to generate these embeddings.", + [TaskType.LLM_GET_EMBEDDINGS]: + "A system task to get the numerical vector representations of words, phrases, sentences, or documents that have been previously learned or generated by the model. Unlike the process of generating embeddings (LLM Generate Embeddings task), which involves creating vector representations from input data, this task deals with the retrieval of pre-existing embeddings and uses them to search for data in vector databases.", + [TaskType.LLM_STORE_EMBEDDINGS]: + "A system task responsible for storing the generated embeddings produced by the LLM Generate Embeddings task, into a vector database. The stored embeddings serve as a repository of information that can be later accessed by the LLM Get Embeddings task for efficient and quick retrieval of related data.", + [TaskType.LLM_SEARCH_INDEX]: + "A system task to search the vector database or repository of vector embeddings of already processed and indexed documents to get the closest match. You can input a query that typically refers to a question, statement, or request made in natural language that is used to search, retrieve, or manipulate data stored in a database.", + [TaskType.LLM_INDEX_DOCUMENT]: + "A system task to index the provided document into a vector database that can be efficiently searched, retrieved, and processed later.", + [TaskType.GET_DOCUMENT]: + "A system task to retrieve the content of the document provided and use it for further data processing using AI tasks.", + [TaskType.LLM_INDEX_TEXT]: + "A system task to index the provided text into a vector space that can be efficiently searched, retrieved, and processed later.", + [TaskType.LLM_CHAT_COMPLETE]: + "A system task to complete the chat query. It can be used to instruct the model's behavior accurately to prevent any deviation from the objective.", +}; diff --git a/ui-next/src/pages/definition/EditorPanel/WorkflowPropertiesFormTab/ActorToHandlerValue.tsx b/ui-next/src/pages/definition/EditorPanel/WorkflowPropertiesFormTab/ActorToHandlerValue.tsx new file mode 100644 index 0000000000..4cbb0d32f0 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/WorkflowPropertiesFormTab/ActorToHandlerValue.tsx @@ -0,0 +1,34 @@ +import { useSelector } from "@xstate/react"; +import { ReactNode } from "react"; +import { ActorRef } from "xstate"; +import { + MetadataFieldMachineEventTypes, + MetdataFieldMachineEvents, +} from "./state"; + +interface ChildrenProps { + onChange: (value: any) => void; + value: any; + someKey: string; +} + +export interface ActorToHandlerValueProps { + children: (props: ChildrenProps) => ReactNode; + actor: ActorRef; +} + +export const ActorToHandlerValue = ({ + actor, + children, +}: ActorToHandlerValueProps) => { + const send = actor.send; + const value = useSelector(actor, (state) => state.context.value); + const someKey = useSelector(actor, (state) => state.context.someKey); + const handleValueChange = (value: any) => { + send({ + type: MetadataFieldMachineEventTypes.CHANGE_VALUE, + value, + }); + }; + return children({ onChange: handleValueChange, value, someKey }); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/WorkflowPropertiesFormTab/WorkflowPropertiesForm.tsx b/ui-next/src/pages/definition/EditorPanel/WorkflowPropertiesFormTab/WorkflowPropertiesForm.tsx new file mode 100644 index 0000000000..c5557f429c --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/WorkflowPropertiesFormTab/WorkflowPropertiesForm.tsx @@ -0,0 +1,530 @@ +import { + Box, + FormControlLabel, + Grid, + Paper, + Stack, + Switch, + Tab, + Tabs, +} from "@mui/material"; +import { PanelAccordion } from "components/PanelAccordion"; +import { ConductorAutoComplete } from "components/v1"; +import ConductorInput from "components/v1/ConductorInput"; +import { ConductorStringArrayFormField } from "components/v1/ConductorStringArrayFormField"; +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import { ConductorFlatMapFormBase } from "components/v1/FlatMapForm/ConductorFlatMapForm"; +import _clone from "lodash/clone"; +import { pluginRegistry } from "plugins/registry"; +import { + WorkflowMetadataEvents, + WorkflowMetadataProvider, +} from "pages/definition/WorkflowMetadata/state"; +import { FunctionComponent, useCallback, useState } from "react"; +import { MetadataBanner } from "shared/createAndDisplayApplication/MetadataBanner"; +import { borders, colors } from "theme/tokens/variables"; +import { printableUpdatedTime } from "utils"; +import { useEventNameSuggestions } from "utils/hooks"; +import { useLazyWorkflowNameAutoComplete } from "utils/useLazyWorkflowNameAutoComplete"; +import { ActorRef } from "xstate"; +import RateLimitConfigForm from "../TaskFormTab/forms/RateLimitConfigForm"; +import { SchemaForm } from "../TaskFormTab/forms/SchemaForm"; +import TaskFormSection from "../TaskFormTab/forms/TaskFormSection"; +import { ActorToHandlerValue } from "./ActorToHandlerValue"; +import { useWorkflowMetadata, useWorkflowMetadataEditorActor } from "./state"; + +// import { FEATURES, featureFlags } from "utils"; + +// const isPlayground = featureFlags.isEnabled(FEATURES.PLAYGROUND); +const ownerChipStyle = { + padding: "2px 10px", + background: colors.roleReadOnly, + borderRadius: "100px", + fontSize: "12px", + color: colors.sidebarBlacky, + fontWeight: 500, +}; + +export interface WorkflowPropertiesFormProps { + workflowMetadataActor: ActorRef; +} +const minWidthTimeout = "170px"; +const timeoutPolicies = [ + { + label: "Timeout Workflow", + value: "TIME_OUT_WF", + }, + { + label: "Alert Only", + value: "ALERT_ONLY", + }, +]; +const getTimeoutPolicyLabel = (option: any) => { + const item = timeoutPolicies.find((x) => x.value === option); + return item ? item.label : ""; +}; + +export const WorkflowPropertiesForm: FunctionComponent< + WorkflowPropertiesFormProps +> = ({ workflowMetadataActor }) => { + const [ + { + // DEPRECATED. DONT USE THIS HOOK ANYMORE, USE THE ONE BELOW + // This was from an old version the spawning of the actors made sense back then given the posistion of the title and other attributes + // This changed the metadata is a tab now. + inputParametersActor, + outputParametersActors, + restartableActors, + timeoutSecondsActors, + timeoutPolicyActors, + failureWorkflowActors, + isReady, + nameFieldActor, + descriptionFieldActor, + workflowStatusListenerEnabledActor, + workflowStatusListenerSinkActor, + rateLimitConfigActor, + }, + ] = useWorkflowMetadataEditorActor(workflowMetadataActor); + + const [ + { + currentWorkflowName, + ownerEmail, + wUpdateTime, + workflowStatusListenerEnabled, + fastAppCreation, + installScriptMetadata, + readmeMetadata, + inputSchema, + outputSchema, + enforceSchema, + }, + { removeMetadataAttribs, updateSchemaForm }, + ] = useWorkflowMetadata(workflowMetadataActor); + + const updatedTime: string = printableUpdatedTime(wUpdateTime); + + const filterCurrentWorkflowOut = useCallback( + (x: string) => x !== currentWorkflowName, + [currentWorkflowName], + ); + + const [fetch, wfNameOptions] = useLazyWorkflowNameAutoComplete( + filterCurrentWorkflowOut, + ); + + const workflowListenerSinkSuggestions = useEventNameSuggestions(); + + const [activeTab, setActiveTab] = useState(0); + + const createAndDisplayAppActor = + workflowMetadataActor.getSnapshot().children[ + "createAndDisplayApplicationMachine" + ]; + return ( + + + {isReady && ( + + + + + + + + + {`Last updated ${updatedTime}`} + {ownerEmail} + + + + + {fastAppCreation && + createAndDisplayAppActor && + (() => { + const GeneratedKeyDialog = + pluginRegistry.getGeneratedKeyDialog(); + // Only show MetadataBanner if the GeneratedKeyDialog is available (enterprise) + if (!GeneratedKeyDialog) return null; + return ( + removeMetadataAttribs()} + installScript={installScriptMetadata} + KeysDisplayerComponent={({ onClose, accessKeys }) => ( + {}} + /> + )} + /> + ); + })()} + + + + + .MuiAccordion-root:not(:first-of-type)": { + borderTop: "1px solid #ddd", + }, + "& > .MuiAccordion-root:first-of-type, & > .MuiAccordion-root:first-of-type:hover": + { + borderTopLeftRadius: borders.radiusSmall, + borderTopRightRadius: borders.radiusSmall, + }, + "& > .MuiAccordion-root:first-of-type .MuiAccordionSummary-root, & > .MuiAccordion-root:first-of-type:hover .MuiAccordionSummary-root": + { + borderTopLeftRadius: borders.radiusSmall, + borderTopRightRadius: borders.radiusSmall, + }, + "& > .MuiAccordion-root:last-of-type, & > .MuiAccordion-root:last-of-type:hover": + { + borderBottomLeftRadius: borders.radiusSmall, + borderBottomRightRadius: borders.radiusSmall, + }, + "& > .MuiAccordion-root:last-of-type .MuiAccordionSummary-root, & > .MuiAccordion-root:last-of-type:hover .MuiAccordionSummary-root": + { + borderBottomLeftRadius: borders.radiusSmall, + borderBottomRightRadius: borders.radiusSmall, + }, + }} + > + + + + + + {({ onChange, value: name }) => ( + onChange(value)} + helperText="Workflow name must be unique." + fullWidth + id="workflow-name-field" + /> + )} + + + + + {({ onChange, value: description }) => ( + onChange(value)} + fullWidth + required + multiline={true} + rows={3} + error={!description} + autoFocus + placeholder="Enter description" + /> + )} + + + + + + + + setActiveTab(newValue)} + aria-label="schema and parameters tabs" + > + + + + + {activeTab === 0 && ( + <> + + + {({ onChange, value: inputParameters, someKey }) => ( + + )} + + + + + {({ onChange, value: outputParameters, someKey }) => ( + + } //Patch this aint A FIX you can put json as an output param + keyColumnLabel="Parameter" + valueColumnLabel="Value" + addItemLabel="Add parameter" + onChange={onChange} + someKey={someKey} + emptyListMessage="These values serve as an indicator of what outputs this workflow will produce." + compact + /> + )} + + + + )} + {activeTab === 1 && ( + { + updateSchemaForm( + value?.inputSchema, + value?.outputSchema, + value?.enforceSchema, + ); + }} + /> + )} + + + + + + + + {({ + onChange, + value: workflowStatusListenerEnabledValue, + }) => ( + + onChange(checked) + } + /> + } + label="Enable workflow status listener" + /> + )} + + + {workflowStatusListenerEnabled && ( + + + {({ onChange, value: workflowListenerSink }) => ( + + )} + + + {/* description */} + + + )} + + + + + + + + {({ onChange, value: timeoutSeconds }) => ( + -1 ? timeoutSeconds : ""} + onTextInputChange={(timeout) => + onChange(parseInt(timeout)) + } + /> + )} + + + + + {({ onChange, value: timeoutPolicy }) => ( + { + onChange(val); + }} + getOptionLabel={(option) => + getTimeoutPolicyLabel(option) + } + renderOption={(props, option) => ( +
  • + {getTimeoutPolicyLabel(option)} +
  • + )} + options={timeoutPolicies.map((x) => x.value)} + autoComplete + includeInputInList + label="Timeout policy" + /> + )} +
    +
    +
    +
    + + + + + {({ onChange, value: restartable }) => ( + + onChange(checked) + } + /> + } + label="Allow workflow restarts" + /> + )} + + + When enabled, completed workflows can be restarted. + Disable this option if restarting a workflow could + cause side effects. + + + + + + + + + {({ onChange, value: failureWorkflow }) => ( + + )} + + + If present, this workflow will be triggered upon a + failure of the execution of this workflow. + + + + + + + + + {({ onChange, value: rateLimitConfig }) => ( + + )} + + + +
    +
    +
    +
    + )} +
    +
    + ); +}; diff --git a/ui-next/src/pages/definition/EditorPanel/WorkflowPropertiesFormTab/index.ts b/ui-next/src/pages/definition/EditorPanel/WorkflowPropertiesFormTab/index.ts new file mode 100644 index 0000000000..3fe72e26e5 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/WorkflowPropertiesFormTab/index.ts @@ -0,0 +1 @@ +export * from "./WorkflowPropertiesForm"; diff --git a/ui-next/src/pages/definition/EditorPanel/WorkflowPropertiesFormTab/state/actions.ts b/ui-next/src/pages/definition/EditorPanel/WorkflowPropertiesFormTab/state/actions.ts new file mode 100644 index 0000000000..91980ef633 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/WorkflowPropertiesFormTab/state/actions.ts @@ -0,0 +1,25 @@ +import { assign, sendParent } from "xstate"; +import { cancel } from "xstate/lib/actions"; +import { MetadataFieldMachineContext, ChangeValueEvent } from "./types"; +import { WorkflowMetadataMachineEventTypes } from "pages/definition/WorkflowMetadata/state/types"; + +export const persistChanges = assign< + MetadataFieldMachineContext, + ChangeValueEvent +>({ + value: (_context, { value }) => value, +}); + +export const addSomeKey = assign({ + someKey: (_context) => Math.random().toString(36).substring(2, 7), // hack for json components. to re-render on external value change +}); + +export const debounceSyncWithParent = sendParent( + (context: MetadataFieldMachineContext, { value }: ChangeValueEvent) => ({ + type: WorkflowMetadataMachineEventTypes.UPDATE_METADATA, + metadataChanges: { [context.fieldName]: value }, + }), + /* { delay: 500, id: "sync_val_with_parent" } */ +); + +export const cancelSyncWithParent = cancel("sync_val_with_parent"); diff --git a/ui-next/src/pages/definition/EditorPanel/WorkflowPropertiesFormTab/state/hook.ts b/ui-next/src/pages/definition/EditorPanel/WorkflowPropertiesFormTab/state/hook.ts new file mode 100644 index 0000000000..4c4dbdafcb --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/WorkflowPropertiesFormTab/state/hook.ts @@ -0,0 +1,145 @@ +import { ActorRef } from "xstate"; +import { useSelector } from "@xstate/react"; +import { + WorkflowMetadataEvents, + WorkflowMetadataMachineEventTypes, +} from "pages/definition/WorkflowMetadata/state"; +import { SchemaFormValue } from "../../TaskFormTab/forms/SchemaForm"; + +export const useWorkflowMetadataEditorActor = ( + metadataEditorActor: ActorRef, +) => { + const [ + inputParametersActor, + outputParametersActors, + restartableActors, + timeoutSecondsActors, + timeoutPolicyActors, + failureWorkflowActors, + nameFieldActor, + descriptionFieldActor, + inputSchemaFieldActor, + outputSchemaFieldActor, + enforceSchemaFieldActor, + workflowStatusListenerEnabledActor, + workflowStatusListenerSinkActor, + rateLimitConfigActor, + ] = useSelector( + metadataEditorActor, + (state) => state.context.editableFieldActors, + ); + + const isReady = useSelector(metadataEditorActor, (state) => + state.hasTag("editingEnabled"), + ); + + return [ + { + inputParametersActor, + outputParametersActors, + restartableActors, + timeoutSecondsActors, + timeoutPolicyActors, + failureWorkflowActors, + isReady, + nameFieldActor, + descriptionFieldActor, + inputSchemaFieldActor, + outputSchemaFieldActor, + enforceSchemaFieldActor, + workflowStatusListenerEnabledActor, + workflowStatusListenerSinkActor, + rateLimitConfigActor, + }, + ]; +}; + +export const useWorkflowMetadata = ( + metadataEditorActor: ActorRef, +) => { + const wUpdateTime = useSelector( + metadataEditorActor, + (state) => state.context?.metadataChanges?.updateTime, + ); + const ownerEmail = useSelector( + metadataEditorActor, + (state) => state.context?.metadataChanges?.ownerEmail, + ); + const currentWorkflowName = useSelector( + metadataEditorActor, + (state) => state.context.metadata.name, + ); + + const workflowStatusListenerEnabled = useSelector( + metadataEditorActor, + (state) => state.context.metadataChanges.workflowStatusListenerEnabled, + ); + + const fastAppCreation = useSelector(metadataEditorActor, (state) => + state.hasTag("fastAppCreation"), + ); + + const installScriptMetadata = useSelector( + metadataEditorActor, + (state) => state.context.metadataChanges?.metadata?.installScript, + ); + const readmeMetadata = useSelector( + metadataEditorActor, + (state) => state.context.metadataChanges?.metadata?.readme, + ); + + const inputSchema = useSelector( + metadataEditorActor, + (state) => state.context.metadataChanges?.inputSchema, + ); + const outputSchema = useSelector( + metadataEditorActor, + (state) => state.context.metadataChanges?.outputSchema, + ); + const enforceSchema = useSelector( + metadataEditorActor, + (state) => state.context.metadataChanges?.enforceSchema, + ); + + const removeMetadataAttribs = () => { + metadataEditorActor.send({ + type: WorkflowMetadataMachineEventTypes.UPDATE_METADATA, + metadataChanges: { + metadata: {}, + }, + }); + }; + const updateSchemaForm = ( + inputSchema?: SchemaFormValue, + outputSchema?: SchemaFormValue, + enforceSchema?: boolean, + ) => { + metadataEditorActor.send({ + type: WorkflowMetadataMachineEventTypes.UPDATE_METADATA, + metadataChanges: { + inputSchema: inputSchema as unknown as Record, + outputSchema: outputSchema as unknown as Record, + enforceSchema: enforceSchema as unknown as boolean, + }, + }); + }; + + return [ + { + wUpdateTime, + ownerEmail, + currentWorkflowName, + workflowStatusListenerEnabled, + fastAppCreation, + installScriptMetadata, + readmeMetadata, + inputSchema, + outputSchema, + enforceSchema, + }, + { + removeMetadataAttribs, + updateSchemaForm, + }, + ] as const; +}; diff --git a/ui-next/src/pages/definition/EditorPanel/WorkflowPropertiesFormTab/state/index.ts b/ui-next/src/pages/definition/EditorPanel/WorkflowPropertiesFormTab/state/index.ts new file mode 100644 index 0000000000..4ef79909e8 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/WorkflowPropertiesFormTab/state/index.ts @@ -0,0 +1,3 @@ +export * from "./types"; +export * from "./machine"; +export * from "./hook"; diff --git a/ui-next/src/pages/definition/EditorPanel/WorkflowPropertiesFormTab/state/machine.ts b/ui-next/src/pages/definition/EditorPanel/WorkflowPropertiesFormTab/state/machine.ts new file mode 100644 index 0000000000..a315aa3e25 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/WorkflowPropertiesFormTab/state/machine.ts @@ -0,0 +1,40 @@ +import { createMachine } from "xstate"; +import * as actions from "./actions"; +import { + MetadataFieldMachineContext, + MetadataFieldMachineEventTypes, + MetdataFieldMachineEvents, +} from "./types"; + +export const metadataFieldMachine = createMachine< + MetadataFieldMachineContext, + MetdataFieldMachineEvents +>( + { + id: "workflowMetadataField", + initial: "focused", + predictableActionArguments: true, + context: { + value: "", + fieldName: "", + someKey: "", + }, + on: { + [MetadataFieldMachineEventTypes.VALUE_UPDATED]: { + actions: ["persistChanges", "addSomeKey"], + }, + }, + states: { + focused: { + on: { + [MetadataFieldMachineEventTypes.CHANGE_VALUE]: { + actions: ["persistChanges", "debounceSyncWithParent"], + }, + }, + }, + }, + }, + { + actions: actions as any, + }, +); diff --git a/ui-next/src/pages/definition/EditorPanel/WorkflowPropertiesFormTab/state/types.ts b/ui-next/src/pages/definition/EditorPanel/WorkflowPropertiesFormTab/state/types.ts new file mode 100644 index 0000000000..d4207177a7 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/WorkflowPropertiesFormTab/state/types.ts @@ -0,0 +1,28 @@ +export interface MetadataFieldMachineContext { + value: string; + fieldName: string; + someKey?: string; +} + +export enum MetadataFieldMachineEventTypes { + // TOGGLE_EDITING = "TOGGLE_EDITING", + CHANGE_VALUE = "CHANGE_VALUE", + VALUE_UPDATED = "VALUE_UPDATED", + // DISABLE_EDITING = "DISABLE_EDITING", +} + +export type ChangeValueEvent = { + type: MetadataFieldMachineEventTypes.CHANGE_VALUE; + value: string; +}; + +export type ValueUpdatedEvent = { + type: MetadataFieldMachineEventTypes.VALUE_UPDATED; + value: string; +}; + +// export type DisableEditingEvent = { +// type: EditInPlaceEventTypes.DISABLE_EDITING; +// }; + +export type MetdataFieldMachineEvents = ChangeValueEvent | ValueUpdatedEvent; diff --git a/ui-next/src/pages/definition/EditorPanel/hook.ts b/ui-next/src/pages/definition/EditorPanel/hook.ts new file mode 100644 index 0000000000..5758602dff --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/hook.ts @@ -0,0 +1,92 @@ +import { ActorRef } from "xstate"; +import { useSelector } from "@xstate/react"; + +import fastDeepEqual from "fast-deep-equal"; + +import { + DefinitionMachineEventTypes, + WorkflowDefinitionEvents, +} from "../state/types"; +import { usePanelChanges } from "pages/definition/state/usePanelChanges"; +import { + isSaveRequestSelector, + versionSelector, + versionsSelector, +} from "./selectors"; + +export const useDefinitionMachine = ( + service: ActorRef, +) => { + const handleConfirmReset = () => + service.send({ type: DefinitionMachineEventTypes.RESET_CONFIRM_EVT }); + + const handleChangeVersion = (version: string) => + service.send({ + type: DefinitionMachineEventTypes.CHANGE_VERSION_EVT, + version, + }); + + const handleConfirmDelete = () => + service.send({ type: DefinitionMachineEventTypes.DELETE_CONFIRM_EVT }); + + const handleCancelRequest = () => + service.send({ type: DefinitionMachineEventTypes.CANCEL_EVENT_EVT }); + + const handleConfirmLastForkRemovalRequest = () => + service.send({ + type: DefinitionMachineEventTypes.CONFIRM_LAST_FORK_REMOVAL, + }); + + const isConfirmDelete = useSelector(service, (state) => + state.matches("ready.rightPanel.opened.confirmDelete"), + ); + + const version = useSelector(service, versionSelector); + + const versions = useSelector(service, versionsSelector, fastDeepEqual); + + const isConfirmReset = useSelector(service, (state) => + state.matches("ready.rightPanel.opened.confirmReset"), + ); + + const isSaveRequest = useSelector(service, isSaveRequestSelector); + + const isRunWorkflow = useSelector(service, (state) => + state.matches("ready.rightPanel.opened.runWorkflow"), + ); + + const isConfirmingForkRemoval = useSelector(service, (state) => + state.matches("ready.diagram.branchRemoval.confirmForkJoinRemoval"), + ); + + const openedTab = useSelector(service, (state) => state.context.openedTab); + + const changeTab = (tab: number) => { + service.send({ type: DefinitionMachineEventTypes.CHANGE_TAB_EVT, tab }); + }; + + const { leftPanelExpanded, setLeftPanelExpanded } = usePanelChanges(service); + + return [ + { + handleConfirmReset, + handleChangeVersion, + handleConfirmDelete, + handleCancelRequest, + handleConfirmLastForkRemovalRequest, + changeTab, + setLeftPanelExpanded, + }, + { + isConfirmDelete, + version, + versions, + isConfirmReset, + openedTab, + isSaveRequest, + isConfirmingForkRemoval, + leftPanelExpanded, + isRunWorkflow, + }, + ] as const; +}; diff --git a/ui-next/src/pages/definition/EditorPanel/selectors.ts b/ui-next/src/pages/definition/EditorPanel/selectors.ts new file mode 100644 index 0000000000..dbc383bab7 --- /dev/null +++ b/ui-next/src/pages/definition/EditorPanel/selectors.ts @@ -0,0 +1,11 @@ +import { State } from "xstate"; +import { DefinitionMachineContext } from "../state"; + +export const versionSelector = (state: State) => + state.context.currentVersion; + +export const versionsSelector = (state: State) => + state.context.workflowVersions; + +export const isSaveRequestSelector = (state: State) => + state.hasTag("saveRequest"); diff --git a/ui-next/src/pages/definition/EventHandler/EventHandler.tsx b/ui-next/src/pages/definition/EventHandler/EventHandler.tsx new file mode 100644 index 0000000000..f3e7cf4de5 --- /dev/null +++ b/ui-next/src/pages/definition/EventHandler/EventHandler.tsx @@ -0,0 +1,224 @@ +import { Box, CircularProgress, Paper, Tab, Tabs } from "@mui/material"; +import { DocLink } from "components/DocLink"; +import ConfirmChoiceDialog from "components/ConfirmChoiceDialog"; +import { SnackbarMessage } from "components/SnackbarMessage"; +import { ConductorSectionHeader } from "components/v1/layout/section/ConductorSectionHeader"; +import EventHandlerButton from "pages/definition/EventHandler/eventhandlers/EventHandlerButton"; +import EventHandlerEditor from "pages/definition/EventHandler/eventhandlers/EventHandlerEditor"; +import EventHandlerForm from "pages/definition/EventHandler/eventhandlers/FormComponent/EventHandlerForm"; +import { useEventHandlerDefinition } from "pages/definition/EventHandler/eventhandlers/state/hook"; +import { Helmet } from "react-helmet"; +import SectionContainer from "shared/SectionContainer"; +import { colors } from "theme/tokens/variables"; +import { DOC_LINK_URL } from "utils/constants/docLink"; +import { EVENT_HANDLERS_URL } from "utils/constants/route"; +import { ActorRef } from "xstate"; +import { FormHandlerEvents } from "./eventhandlers/FormComponent/state/types"; +import { SaveProtectionPrompt } from "./SaveProtectionPrompt"; + +export default function EventHandlerDefinition() { + const [ + { + handleSaveRequest, + handleCancelRequest, + handleResetRequest, + handleEditChanges, + handleConfirmSaveRequest, + handleConfirmReset, + handleDeleteRequest, + handleConfirmDelete, + handleBackToIdle, + handleClearErrorMessage, + toggleFormMode, + service, + }, + { + isNewEventHandler, + editorChanges, + isConfirmSave, + isConfirmReset, + isSaving, + originalSource, + isConfirmDelete, + madeChanges, + message, + eventHandlerName, + isFormMode, + couldNotParseJson, + isEditorMode, + isFetching, + }, + ] = useEventHandlerDefinition(); + + return ( + + {isConfirmReset && ( + { + if (confirmed) { + handleConfirmReset?.(); + } else { + handleBackToIdle?.(); + } + }} + message={ + "You will lose all changes made in the editor. Please confirm resetting this Event Handler definition to its original state." + } + /> + )} + + {isConfirmDelete && ( + { + if (confirmed) { + handleConfirmDelete?.(); + } else { + handleBackToIdle?.(); + } + }} + message={ + <> + Are you sure you want to delete{" "} + {eventHandlerName} Event + Handler definition? This change cannot be undone. +
    + Please type {eventHandlerName} to confirm +
    + + } + valueToBeDeleted={eventHandlerName} + isInputConfirmation + /> + )} + + + Event Handler Definition -  + {eventHandlerName ? eventHandlerName : "NEW"} + + + + + } + /> + } + > + + {message && ( + handleClearErrorMessage()} + /> + )} + + + + + + + + + + {isFetching ? ( + + + + ) : ( + + theme.palette?.mode === "dark" ? colors.gray14 : undefined, + backgroundColor: (theme) => theme.palette.customBackground.form, + }} + > + {isFormMode ? ( + + } + /> + ) : ( + + )} + + )} + + +
    + ); +} diff --git a/ui-next/src/pages/definition/EventHandler/SaveProtectionPrompt.tsx b/ui-next/src/pages/definition/EventHandler/SaveProtectionPrompt.tsx new file mode 100644 index 0000000000..6c22d4be5e --- /dev/null +++ b/ui-next/src/pages/definition/EventHandler/SaveProtectionPrompt.tsx @@ -0,0 +1,173 @@ +import { useSelector } from "@xstate/react"; +import fastDeepEqual from "fast-deep-equal"; +import { omit } from "lodash"; +import { FunctionComponent } from "react"; +import BlockNavigationWithConfirmation from "shared/BlockNavigationWithConfirmation"; +import { useSaveProtection } from "shared/useSaveProtection"; +import { ActorRef, AnyEventObject } from "xstate"; +import { + SaveEventHandlerEvents, + SaveEventHandlerMachineContext, + SaveEventHandlerMachineEventTypes, + SaveEventHandlerStates, +} from "./eventhandlers/state"; + +export interface SaveProtectionPromptProps { + service: ActorRef; +} + +const useCheckForChanges = ( + formActor: ActorRef | null, + editorActor: ActorRef, +) => { + // Always call hooks unconditionally - use editorActor as fallback if formActor is null + const formData = useSelector( + formActor || editorActor, + (state: { + context: { eventAsJson?: unknown; originalSource?: unknown }; + }) => { + // Check if this is form actor context + if (state.context.eventAsJson !== undefined) { + return { + eventAsJson: state.context.eventAsJson, + originalSource: state.context.originalSource, + }; + } + return null; + }, + ); + + const [editorChanges, editorOriginalSource] = useSelector( + editorActor, + (state) => [state.context.editorChanges, state.context.originalSource], + ); + + // Use form data if formActor exists and we have form data, otherwise use editor data + if ( + formActor && + formData && + formData.eventAsJson && + formData.originalSource + ) { + return fastDeepEqual( + omit(formData.eventAsJson as Record, "action"), + formData.originalSource, + ); + } else { + return fastDeepEqual(editorChanges, editorOriginalSource); + } +}; + +export const SaveProtectionPrompt: FunctionComponent< + SaveProtectionPromptProps +> = ({ service }) => { + // @ts-expect-error - children type is not fully typed + const formActor = service?.children?.get("eventFormMachine") as + | ActorRef + | undefined; + + const noFormChanges = useCheckForChanges(formActor || null, service); + + const { + showPrompt, + successfulSave, + hasErrors, + handleSave: baseHandleSave, + } = useSaveProtection( + { + actor: service, + noFormChanges, + isSaveInProgress: (state) => { + // Check if we're in CONFIRM_SAVE state (save confirmation dialog) or saving states + return ( + state.matches(SaveEventHandlerStates.CONFIRM_SAVE) || + state.matches(SaveEventHandlerStates.CREATE_EVENT_HANDLER) || + state.matches(SaveEventHandlerStates.UPDATE_EVENT_HANDLER) + ); + }, + hasErrors: (state) => { + const context = state.context; + + // Check for parse errors + if (context.couldNotParseJson) { + return true; + } + + // Check for API errors + if (context.message) { + return true; + } + + return false; + }, + detectSaveSuccessFromEvent: (eventType) => { + // Check for successful save event + if (eventType === SaveEventHandlerMachineEventTypes.SAVED_SUCCESSFUL) { + return true; + } + // Check for cancelled save event + if (eventType === SaveEventHandlerMachineEventTypes.SAVED_CANCELLED) { + return false; + } + return undefined; + }, + detectSaveSuccessFromContext: ({ + currentContext, + previousContext, + wasSaving, + isSaving, + }) => { + // If we were saving and now we're not, check if originalSource was updated + if (wasSaving && !isSaving && previousContext) { + const currentOriginStr = currentContext.originalSource; + const prevOriginStr = previousContext.originalSource; + + // If origin was updated, save was successful + if (currentOriginStr !== prevOriginStr) { + return true; + } + } + return false; + }, + handleSaveAction: (actor) => { + // Check current state to see if we're already in the save confirmation dialog + const snapshot = actor.getSnapshot(); + const isInSaveConfirmation = snapshot.matches( + SaveEventHandlerStates.CONFIRM_SAVE, + ); + + // If we're already in the save confirmation dialog, trigger the save immediately + if (isInSaveConfirmation) { + actor.send({ + type: SaveEventHandlerMachineEventTypes.CONFIRM_SAVE_EVT, + }); + } else { + // Open the save confirmation dialog + // User will need to click "Confirm Save" button in the save confirmation dialog + actor.send({ + type: SaveEventHandlerMachineEventTypes.SAVE_EVT, + }); + } + }, + }, + ); + + const handleSave = baseHandleSave; + + return ( + + Your recent changes are not saved to the server. To run the new event + handler, you have to save your progress. + + } + title={"Unsaved event handler confirmation"} + block={showPrompt} + onSave={handleSave} + successfulSave={successfulSave} + hasErrors={hasErrors} + /> + ); +}; diff --git a/ui-next/src/pages/definition/EventHandler/eventhandlers/EventHandlerButton.tsx b/ui-next/src/pages/definition/EventHandler/eventhandlers/EventHandlerButton.tsx new file mode 100644 index 0000000000..a1ba8dc178 --- /dev/null +++ b/ui-next/src/pages/definition/EventHandler/eventhandlers/EventHandlerButton.tsx @@ -0,0 +1,210 @@ +import { Box, Stack, Tooltip } from "@mui/material"; +import { useSelector } from "@xstate/react"; +import { FunctionComponent, useMemo } from "react"; +import fastDeepEqual from "fast-deep-equal"; +import { omit } from "lodash"; +import { colors } from "theme/tokens/variables"; +import Button, { MuiButtonProps } from "components/MuiButton"; +import _isEmpty from "lodash/isEmpty"; +import { tryToJson } from "utils/utils"; +import ResetIcon from "components/v1/icons/ResetIcon"; +import SaveIcon from "components/v1/icons/SaveIcon"; +import TrashIcon from "components/v1/icons/TrashIcon"; +import XCloseIcon from "components/v1/icons/XCloseIcon"; +import { useAuth } from "shared/auth"; + +const withFormState = + ( + ButtonComponent: FunctionComponent, + actor: any, + isTrialExpired: boolean, + ) => + (buttonProps: MuiButtonProps) => { + const [eventAsJson, originalSource] = useSelector(actor, (state: any) => [ + state.context.eventAsJson, + state.context.originalSource, + ]); + const noChanges = useMemo( + () => fastDeepEqual(omit(eventAsJson, "action"), originalSource), + [eventAsJson, originalSource], + ); + const { name, event } = eventAsJson; + const emptyValue = [event?.trim(), name?.trim()].some((value) => + _isEmpty(value?.trim()), + ); + const isReset = buttonProps?.role === "reset"; + const disableSave = emptyValue || noChanges || isTrialExpired; + return ( + + ); + }; + +const withEditorState = + ( + ButtonComponent: FunctionComponent, + actor: any, + isTrialExpired: boolean, + ) => + (buttonProps: MuiButtonProps) => { + const [editorChanges, originalSource, invalidJson] = useSelector( + actor, + (state: any) => [ + state.context.editorChanges, + state.context.originalSource, + state.context.couldNotParseJson, + ], + ); + + const isEmptyValue = useMemo(() => { + if (!editorChanges) return false; + const parsedEditorChanges = tryToJson(editorChanges) as { + name: string; + event: string; + }; + const { name, event } = parsedEditorChanges || {}; + return [event?.trim(), name?.trim()].some((value) => _isEmpty(value)); + }, [editorChanges]); + + const noChanges = useMemo( + () => fastDeepEqual(editorChanges, originalSource), + [editorChanges, originalSource], + ); + const isReset = buttonProps?.role === "reset"; + const disableSave = + noChanges || invalidJson || isEmptyValue || isTrialExpired; + return ( + + ); + }; + +type Props = { + isConfirmSave?: boolean; + isConfirmReset?: boolean; + isSaving?: boolean; + handleConfirmSaveRequest?: () => void; + handleCancelRequest?: () => void; + handleSaveRequest?: () => void; + handleResetRequest?: () => void; + isNewEventHandler?: boolean; + handleDeleteRequest?: () => void; + service: any; + disableDeleteBtn: boolean; +}; + +const EventHandlerButton = ({ + isConfirmSave, + isSaving, + handleConfirmSaveRequest, + handleCancelRequest, + handleSaveRequest, + handleResetRequest, + handleDeleteRequest, + isNewEventHandler, + service, + disableDeleteBtn, +}: Props) => { + const { isTrialExpired } = useAuth(); + const formActor = service?.children?.get("eventFormMachine"); + const isInForm = useSelector(service, (state: any) => + state.matches("idle.form"), + ); + + const SaveResetButton = + isInForm && formActor + ? withFormState(Button, formActor, isTrialExpired) + : withEditorState(Button, service, isTrialExpired); + + return ( + + theme.palette?.mode === "dark" ? colors.gray14 : undefined, + backgroundColor: (theme) => + theme.palette?.mode === "dark" ? colors.gray00 : colors.gray14, + }} + > + + {(isConfirmSave || isSaving) && ( + + + + + )} + {!isConfirmSave && !isSaving && ( + <> + + {!isNewEventHandler && ( + + + + )} + + } + > + Reset + + + } + > + Save + + + + )} + + + ); +}; + +export default EventHandlerButton; diff --git a/ui-next/src/pages/definition/EventHandler/eventhandlers/EventHandlerEditor.tsx b/ui-next/src/pages/definition/EventHandler/eventhandlers/EventHandlerEditor.tsx new file mode 100644 index 0000000000..5fdd7a7a06 --- /dev/null +++ b/ui-next/src/pages/definition/EventHandler/eventhandlers/EventHandlerEditor.tsx @@ -0,0 +1,106 @@ +import Editor from "@monaco-editor/react"; +import { Box } from "@mui/material"; +import { DiffEditor } from "components/DiffEditor/DiffEditor"; +import { useContext, useRef } from "react"; +import { defaultEditorOptions } from "shared/editor"; +import { ColorModeContext } from "theme/material/ColorModeContext"; +import { configureMonaco } from "utils/monacoUtils/CodeEditorUtils"; + +type Props = { + handleEditChanges?: (code: string) => void; + editorChanges?: string; + isConfirmSave?: boolean; + originalSource?: string; +}; + +const EventHandlerEditor = ({ + handleEditChanges, + editorChanges, + isConfirmSave, + originalSource, +}: Props) => { + const { mode } = useContext(ColorModeContext); + const editorTheme = mode === "dark" ? "vs-dark" : "vs-light"; + + const monacoObjects = useRef(null); + + function handleEditorWillMount(monaco: any) { + configureMonaco(monaco); + } + + const handleEditorDidMount = (editor: any) => { + monacoObjects.current = editor; + if (handleEditChanges) { + handleEditChanges(editor.getValue()); + } + + monacoObjects.current.onDidChangeModelContent(() => { + if (handleEditChanges) { + handleEditChanges(editor.getValue()); + } + }); + }; + return ( + <> + + + {isConfirmSave ? ( + + ) : ( + { + if (typeof maybeText === "string") { + if (handleEditChanges) { + handleEditChanges(maybeText); + } + } + }} + /> + )} + + + + ); +}; + +export default EventHandlerEditor; diff --git a/ui-next/src/pages/definition/EventHandler/eventhandlers/FormComponent/ActionForms/CompleteTask.tsx b/ui-next/src/pages/definition/EventHandler/eventhandlers/FormComponent/ActionForms/CompleteTask.tsx new file mode 100644 index 0000000000..e32f7586e6 --- /dev/null +++ b/ui-next/src/pages/definition/EventHandler/eventhandlers/FormComponent/ActionForms/CompleteTask.tsx @@ -0,0 +1,69 @@ +import { Grid } from "@mui/material"; +import IconButton from "components/MuiIconButton"; +import MuiTypography from "components/MuiTypography"; +import { ConductorUpdateTaskFormEvent } from "components/v1/ConductorUpdateTaskFromEvent"; +import { ConductorFlatMapFormBase } from "components/v1/FlatMapForm/ConductorFlatMapForm"; +import XCloseIcon from "components/v1/icons/XCloseIcon"; +import { Props } from "./common"; + +export const CompleteTask = ({ + onRemove, + index, + payload, + handleChangeAction, +}: Props) => { + const { complete_task } = payload; + + return ( + + + + Complete Task + + + + { + handleChangeAction(index, { + ...payload, + complete_task: { ...upCt, output: complete_task?.output }, + }); + }} + /> + + + { + handleChangeAction(index, { + ...payload, + complete_task: { + ...complete_task, + output: newValues, + }, + }); + }} + value={{ ...complete_task?.output }} + title="Output" + keyColumnLabel="Key" + valueColumnLabel="Value" + addItemLabel="Add parameter" + showFieldTypes + enableAutocomplete={false} + autoFocusField={false} + /> + + + + + + ); +}; diff --git a/ui-next/src/pages/definition/EventHandler/eventhandlers/FormComponent/ActionForms/FailTask.tsx b/ui-next/src/pages/definition/EventHandler/eventhandlers/FormComponent/ActionForms/FailTask.tsx new file mode 100644 index 0000000000..c535acd3c0 --- /dev/null +++ b/ui-next/src/pages/definition/EventHandler/eventhandlers/FormComponent/ActionForms/FailTask.tsx @@ -0,0 +1,70 @@ +import { Grid } from "@mui/material"; +import HelperText from "components/HelperText"; +import IconButton from "components/MuiIconButton"; +import MuiTypography from "components/MuiTypography"; +import { ConductorUpdateTaskFormEvent } from "components/v1/ConductorUpdateTaskFromEvent"; +import { ConductorFlatMapFormBase } from "components/v1/FlatMapForm/ConductorFlatMapForm"; +import XCloseIcon from "components/v1/icons/XCloseIcon"; +import { Props } from "./common"; + +export const FailTask = ({ + onRemove, + index, + payload, + handleChangeAction, +}: Props) => { + const { fail_task } = payload; + + return ( + + + + Fail Task + + Choose between one of these options + + + { + handleChangeAction(index, { + ...payload, + fail_task: { ...upCt, output: fail_task?.output }, + }); + }} + /> + + + { + handleChangeAction(index, { + ...payload, + fail_task: { + ...fail_task, + output: newValues, + }, + }); + }} + value={{ ...fail_task?.output }} + title="Output" + keyColumnLabel="Key" + valueColumnLabel="Value" + addItemLabel="Add parameter" + showFieldTypes + enableAutocomplete={false} + autoFocusField={false} + /> + + + + + + + + ); +}; diff --git a/ui-next/src/pages/definition/EventHandler/eventhandlers/FormComponent/ActionForms/StartWorkflowTask.tsx b/ui-next/src/pages/definition/EventHandler/eventhandlers/FormComponent/ActionForms/StartWorkflowTask.tsx new file mode 100644 index 0000000000..0b8b72b2e1 --- /dev/null +++ b/ui-next/src/pages/definition/EventHandler/eventhandlers/FormComponent/ActionForms/StartWorkflowTask.tsx @@ -0,0 +1,235 @@ +import { Grid } from "@mui/material"; +import IconButton from "components/MuiIconButton"; +import MuiTypography from "components/MuiTypography"; +import { ConductorAutoComplete } from "components/v1"; +import ConductorInput from "components/v1/ConductorInput"; +import { ConductorFlatMapFormBase } from "components/v1/FlatMapForm/ConductorFlatMapForm"; +import XCloseIcon from "components/v1/icons/XCloseIcon"; +import _isEmpty from "lodash/isEmpty"; +import _isUndefined from "lodash/isUndefined"; +import { FocusEvent, useMemo } from "react"; +import { useWorkflowNamesAndVersions } from "utils/query"; +import { Props } from "./common"; +import IdempotencyForm from "pages/runWorkflow/IdempotencyForm"; +import { IdempotencyStrategyEnum } from "pages/runWorkflow/types"; +import { IdempotencyValuesProp } from "pages/definition/RunWorkflow/state"; + +export const StartWorkflowActionForm = ({ + onRemove, + index, + payload, + handleChangeAction, +}: Props) => { + const { start_workflow } = payload; + const fetchedNamesAndVersions = useWorkflowNamesAndVersions(); + const options = useMemo( + () => + fetchedNamesAndVersions.size === 0 + ? [] + : Array.from(fetchedNamesAndVersions.keys()), + [fetchedNamesAndVersions], + ); + + const maybeSelectedWorkflowName = useMemo( + () => (_isEmpty(start_workflow?.name) ? undefined : start_workflow?.name), + [start_workflow?.name], + ); + + const availableVersions: string[] = useMemo(() => { + const versions: number[] = + fetchedNamesAndVersions.get(maybeSelectedWorkflowName) || []; + + return _isUndefined(maybeSelectedWorkflowName) && !_isEmpty(options) + ? [] + : versions.map((val) => val.toString()); + }, [maybeSelectedWorkflowName, fetchedNamesAndVersions, options]); + + const handleIdempotencyValues = (data: IdempotencyValuesProp) => { + const idempotencyStrategy = () => { + if (data.idempotencyStrategy) { + return data.idempotencyStrategy; + } + if (start_workflow?.idempotencyStrategy) { + return start_workflow?.idempotencyStrategy; + } + return IdempotencyStrategyEnum.RETURN_EXISTING; + }; + + const updatedPayload = { + ...payload, + start_workflow: { + ...start_workflow, + idempotencyKey: data?.idempotencyKey, + idempotencyStrategy: data?.idempotencyKey + ? idempotencyStrategy() + : undefined, + }, + }; + handleChangeAction(index, updatedPayload); + }; + + return ( + + + + Start Workflow + + + + + handleChangeAction(index, { + ...payload, + start_workflow: { + ...start_workflow, + name: value, + }, + }) + } + onBlur={(event: FocusEvent) => { + handleChangeAction(index, { + ...payload, + start_workflow: { + ...start_workflow, + name: event.target.value, + }, + }); + }} + conductorInputProps={{ + placeholder: `\${event.payload.workflow_name}`, + }} + /> + + + + handleChangeAction(index, { + ...payload, + start_workflow: { + ...start_workflow, + version: value, + }, + }) + } + onBlur={(event: FocusEvent) => { + handleChangeAction(index, { + ...payload, + start_workflow: { + ...start_workflow, + version: event.target.value, + }, + }); + }} + conductorInputProps={{ + placeholder: "latest", + }} + /> + + + + handleChangeAction(index, { + ...payload, + start_workflow: { + ...start_workflow, + correlationId: value, + }, + }) + } + /> + + + + + + + + { + handleChangeAction(index, { + ...payload, + start_workflow: { + ...start_workflow, + input: newValues, + }, + }); + }} + value={{ ...start_workflow?.input }} + title="Input variables" + keyColumnLabel="Key" + valueColumnLabel="Value" + addItemLabel="Add parameter" + showFieldTypes + enableAutocomplete={false} + autoFocusField={false} + /> + + + { + handleChangeAction(index, { + ...payload, + start_workflow: { + ...start_workflow, + taskToDomain: newValues, + }, + }); + }} + value={{ ...start_workflow?.taskToDomain }} + title="Tasks to domain mapping" + keyColumnLabel="Key" + valueColumnLabel="Value" + addItemLabel="Add parameter" + showFieldTypes={false} + enableAutocomplete={false} + autoFocusField={false} + /> + + + + + + ); +}; diff --git a/ui-next/src/pages/definition/EventHandler/eventhandlers/FormComponent/ActionForms/TerminateWorkflowTask.tsx b/ui-next/src/pages/definition/EventHandler/eventhandlers/FormComponent/ActionForms/TerminateWorkflowTask.tsx new file mode 100644 index 0000000000..714ee3cec8 --- /dev/null +++ b/ui-next/src/pages/definition/EventHandler/eventhandlers/FormComponent/ActionForms/TerminateWorkflowTask.tsx @@ -0,0 +1,76 @@ +import { Grid } from "@mui/material"; +import IconButton from "components/MuiIconButton"; +import MuiTypography from "components/MuiTypography"; +import ConductorInput from "components/v1/ConductorInput"; +import XCloseIcon from "components/v1/icons/XCloseIcon"; +import { Props } from "./common"; + +export const TerminateWorkflowForm = ({ + onRemove, + index, + payload, + handleChangeAction, +}: Props) => { + const { terminate_workflow } = payload; + + const handleChange = (field: string, value: string) => { + handleChangeAction(index, { + ...payload, + terminate_workflow: { + ...terminate_workflow, + [field]: value, + }, + }); + }; + + return ( + + + + Terminate Workflow + + + + handleChange("workflowId", value)} + /> + + + + handleChange("terminationReason", value) + } + /> + + + + + + ); +}; diff --git a/ui-next/src/pages/definition/EventHandler/eventhandlers/FormComponent/ActionForms/UpdateWorkflowTask.tsx b/ui-next/src/pages/definition/EventHandler/eventhandlers/FormComponent/ActionForms/UpdateWorkflowTask.tsx new file mode 100644 index 0000000000..c96947273d --- /dev/null +++ b/ui-next/src/pages/definition/EventHandler/eventhandlers/FormComponent/ActionForms/UpdateWorkflowTask.tsx @@ -0,0 +1,100 @@ +import { FormControlLabel, Grid } from "@mui/material"; +import HelperText from "components/HelperText"; +import MuiCheckbox from "components/MuiCheckbox"; +import IconButton from "components/MuiIconButton"; +import MuiTypography from "components/MuiTypography"; +import ConductorInput from "components/v1/ConductorInput"; +import { ConductorFlatMapFormBase } from "components/v1/FlatMapForm/ConductorFlatMapForm"; +import XCloseIcon from "components/v1/icons/XCloseIcon"; +import { Props } from "./common"; + +export const UpdateWorkflowForm = ({ + onRemove, + index, + payload, + handleChangeAction, +}: Props) => { + const { update_workflow_variables } = payload; + + return ( + + + + Update Workflow Variables + + + + + handleChangeAction(index, { + ...payload, + update_workflow_variables: { + ...update_workflow_variables, + workflowId: value, + }, + }) + } + /> + + + + handleChangeAction(index, { + ...payload, + update_workflow_variables: { + ...update_workflow_variables, + appendArray: event.target.checked, + }, + }) + } + control={ + + } + label={"Append List Variables (instead of replacing)"} + sx={{ color: "#767676", ">span": { fontWeight: 600 } }} + /> + + If this value is checked, all list (array) variables in the workflow + will be treated as append instead of replace. This can be used to + collect data from a series of events into a single workflow. + + + + { + handleChangeAction(index, { + ...payload, + update_workflow_variables: { + ...update_workflow_variables, + variables: newValues, + }, + }); + }} + value={{ ...update_workflow_variables?.variables }} + title="Output" + keyColumnLabel="Key" + valueColumnLabel="Value" + addItemLabel="Add parameter" + showFieldTypes + enableAutocomplete={false} + autoFocusField={false} + /> + + + + + + ); +}; diff --git a/ui-next/src/pages/definition/EventHandler/eventhandlers/FormComponent/ActionForms/common.ts b/ui-next/src/pages/definition/EventHandler/eventhandlers/FormComponent/ActionForms/common.ts new file mode 100644 index 0000000000..df9b7332af --- /dev/null +++ b/ui-next/src/pages/definition/EventHandler/eventhandlers/FormComponent/ActionForms/common.ts @@ -0,0 +1,45 @@ +export const formContainerStyle = { + marginTop: 5, + marginBottom: 5, + display: "flex", + flexWrap: "wrap", + width: "100%", + gap: "20px", +}; + +export const boxStyle = { + display: "flex", + alignItems: "center", +}; + +export const textFieldStyle = { + ">div": { + width: 220, + }, +}; + +export type Props = { + onRemove?: () => void; + index?: number; + payload?: any; + handleChangeAction?: any; +}; + +export const formBoxStyle = { + width: "calc(100% - 180px)", + "@media screen and (max-width: 860px)": { + width: "100%", + }, +}; + +export const flatMapStyle = { + maxWidth: "100%", +}; + +export const removeButtonStyle = { + height: "fit-content", + position: "relative", + bottom: "0px", + right: "40px", + background: "#e3e3e3", +}; diff --git a/ui-next/src/pages/definition/EventHandler/eventhandlers/FormComponent/EventHandlerForm.tsx b/ui-next/src/pages/definition/EventHandler/eventhandlers/FormComponent/EventHandlerForm.tsx new file mode 100644 index 0000000000..72445dfd0b --- /dev/null +++ b/ui-next/src/pages/definition/EventHandler/eventhandlers/FormComponent/EventHandlerForm.tsx @@ -0,0 +1,258 @@ +import { + Box, + Divider, + FormControlLabel, + Grid, + MenuItem, + Switch, + Theme, + Tooltip, + createFilterOptions, +} from "@mui/material"; +import { Plus } from "@phosphor-icons/react"; +import { ChangeEvent, Fragment } from "react"; +import { ActorRef } from "xstate"; + +import { Button } from "components"; +import { ConductorAutoComplete } from "components/v1"; +import { ConductorCodeBlockInput } from "components/v1/ConductorCodeBlockInput"; +import ConductorInput from "components/v1/ConductorInput"; +import ConductorSelect from "components/v1/ConductorSelect"; +import { colors } from "theme/tokens/variables"; +import { useEventNameSuggestions } from "utils/hooks/useEventNameSuggestions"; +import { CompleteTask } from "./ActionForms/CompleteTask"; +import { FailTask } from "./ActionForms/FailTask"; +import { StartWorkflowActionForm } from "./ActionForms/StartWorkflowTask"; +import { TerminateWorkflowForm } from "./ActionForms/TerminateWorkflowTask"; +import { UpdateWorkflowForm } from "./ActionForms/UpdateWorkflowTask"; +import { useEventHandlerFormActor } from "./state/hook"; +import { Action, FormHandlerEvents, actionLabel } from "./state/types"; + +const containerStyle = { + maxWidth: 818, + color: (theme: Theme) => + theme.palette?.mode === "dark" ? colors.gray14 : undefined, + backgroundColor: (theme: Theme) => theme.palette.customBackground.form, +}; + +const filter = createFilterOptions(); + +const EventHandlerForm = ({ + actor, +}: { + actor: ActorRef; +}) => { + const [ + { action, name, condition, actions, event, active, description }, + { + handleChangeAction, + handleChange, + handleAction, + removeAction, + handleEventChange, + }, + ] = useEventHandlerFormActor(actor); + + const suggestions = useEventNameSuggestions(); + + return ( + <> + + + + + + + + handleChange("name", val)} + /> + + + + handleChange("description", value) + } + value={description} + placeholder="Enter description" + /> + + + handleEventChange(val)} + onInputChange={(_, val) => handleEventChange(val)} + freeSolo + selectOnFocus + filterOptions={(options, params) => { + const filtered = filter(options, params); + + const { inputValue } = params; + // Suggest the creation of a new value + const isExisting = options.some( + (option) => inputValue === option, + ); + + if (inputValue !== "" && !isExisting) { + filtered.push(`${inputValue}`); + } + + return filtered; + }} + /> + + + handleChange("condition", val)} + /> + + + + ) => + handleChange("action", e.target.value) + } + > + {Object.values(Action).map((val) => ( + + {actionLabel[val]} + + ))} + + + + + + + + + + + + + {actions?.map((action: any, index: number) => { + const renderFormComponent = ( + Component: any, + keyPrefix: string, + ) => { + const key = `${keyPrefix}-${index}`; + return ( + + removeAction(index)} + handleChangeAction={handleChangeAction} + payload={action} + index={index} + /> + {index !== actions.length - 1 && ( + + + + )} + + ); + }; + + switch (action.action) { + case Action.FAIL_TASK: + return renderFormComponent(FailTask, `fail_task`); + case Action.START_WORKFLOW: + return renderFormComponent( + StartWorkflowActionForm, + `startWorkflow`, + ); + case Action.TERMINATE_WORKFLOW: + return renderFormComponent( + TerminateWorkflowForm, + `terminate`, + ); + case Action.UPDATE_WORKFLOW_VARIABLES: + return renderFormComponent( + UpdateWorkflowForm, + `updateWf`, + ); + case Action.COMPLETE_TASK: + return renderFormComponent(CompleteTask, `complete`); + default: + return null; + } + })} + + + + + handleChange("active", val.target.checked) + } + /> + } + label="Active" + sx={{ mb: 3 }} + /> + + + + + + + + + + ); +}; + +export default EventHandlerForm; diff --git a/ui-next/src/pages/definition/EventHandler/eventhandlers/FormComponent/state/actions.ts b/ui-next/src/pages/definition/EventHandler/eventhandlers/FormComponent/state/actions.ts new file mode 100644 index 0000000000..e17a10bc18 --- /dev/null +++ b/ui-next/src/pages/definition/EventHandler/eventhandlers/FormComponent/state/actions.ts @@ -0,0 +1,97 @@ +import { + UPDATE_VARIABLES_ACTION, + START_WORKFLOW_ACTION, + TERMINATE_WORKFLOW_ACTION, + COMPLETE_TASK_ACTION, + FAIL_TASK_ACTION, + NEW_EVENT_HANDLER_TEMPLATE, +} from "../../eventHandlerSchema"; +import { Action, EventFormMachineContext } from "./types"; +import { assign } from "xstate"; +import { adjust } from "utils/array"; + +export const handleInputChange = assign((context: any, event: any) => { + const { name, value } = event; + return { + ...context, + eventAsJson: { + ...context.eventAsJson, + [name]: value !== undefined ? value : "", + }, + }; +}); + +export const persistNewAction = assign({ + eventAsJson: (context: any, event: any) => { + const { actionType } = event; + const { actions } = context.eventAsJson; + switch (actionType) { + case Action.COMPLETE_TASK: + return { + ...context.eventAsJson, + actions: [COMPLETE_TASK_ACTION, ...actions], + }; + case Action.TERMINATE_WORKFLOW: + return { + ...context.eventAsJson, + actions: [TERMINATE_WORKFLOW_ACTION, ...actions], + }; + case Action.UPDATE_WORKFLOW_VARIABLES: + return { + ...context.eventAsJson, + actions: [UPDATE_VARIABLES_ACTION, ...actions], + }; + case Action.FAIL_TASK: + return { + ...context.eventAsJson, + actions: [FAIL_TASK_ACTION, ...actions], + }; + case Action.START_WORKFLOW: + return { + ...context.eventAsJson, + actions: [START_WORKFLOW_ACTION, ...actions], + }; + default: + return context.eventAsJson; + } + }, +}); + +export const removeAction = assign({ + eventAsJson: (context: any, event: any) => { + const index = event.index; + const newActions = [...context.eventAsJson.actions]; + newActions.splice(index, 1); + return { + ...context.eventAsJson, + actions: newActions, + }; + }, +}); + +export const editAction = assign({ + eventAsJson: (context: any, event: any) => { + const { index, payload } = event; + return { + ...context.eventAsJson, + actions: adjust( + index, + () => ({ ...payload }), + context.eventAsJson.actions, + ), + }; + }, +}); + +export const resetForm = assign({ + eventAsJson: (context) => { + return context.originalSource; + }, +}); + +export const resetFormToNewDefinition = assign(() => { + return { + originalSource: NEW_EVENT_HANDLER_TEMPLATE, + eventAsJson: NEW_EVENT_HANDLER_TEMPLATE, + }; +}); diff --git a/ui-next/src/pages/definition/EventHandler/eventhandlers/FormComponent/state/hook.ts b/ui-next/src/pages/definition/EventHandler/eventhandlers/FormComponent/state/hook.ts new file mode 100644 index 0000000000..159d5159e7 --- /dev/null +++ b/ui-next/src/pages/definition/EventHandler/eventhandlers/FormComponent/state/hook.ts @@ -0,0 +1,63 @@ +import { useSelector } from "@xstate/react"; +import { EventFormMachineTypes } from "./types"; + +export const useEventHandlerFormActor = (actor: any) => { + const { eventAsJson } = useSelector(actor, (state: any) => state.context); + + const { name, event, condition, actions, action, active, description } = + eventAsJson; + + const { send } = actor; + + const handleChangeAction = (index: number, payload: any) => { + send({ + type: EventFormMachineTypes.EDIT_ACTION, + index, + payload, + }); + }; + + const handleChange = (name: string, value: string | boolean) => { + send({ + type: EventFormMachineTypes.INPUT_CHANGE, + name, + value, + }); + }; + + const handleAction = (action: string) => { + send({ + type: EventFormMachineTypes.ADD_ACTION, + actionType: action, + }); + }; + + const removeAction = (index: number) => { + send({ + type: EventFormMachineTypes.DELETE_ACTION, + index, + }); + }; + + // Logic in the Event task form is similar. Consider refactoring. + const handleEventChange = (event: string) => handleChange("event", event); + + return [ + { + action, + name, + condition, + actions, + event, + active, + description, + }, + { + handleChangeAction, + handleChange, + handleAction, + removeAction, + handleEventChange, + }, + ] as const; +}; diff --git a/ui-next/src/pages/definition/EventHandler/eventhandlers/FormComponent/state/machine.ts b/ui-next/src/pages/definition/EventHandler/eventhandlers/FormComponent/state/machine.ts new file mode 100644 index 0000000000..377da99005 --- /dev/null +++ b/ui-next/src/pages/definition/EventHandler/eventhandlers/FormComponent/state/machine.ts @@ -0,0 +1,63 @@ +import { + EventFormMachineContext, + EventFormMachineStates, + EventFormMachineTypes, + FormHandlerEvents, +} from "./types"; + +import { createMachine } from "xstate"; +import * as actions from "./actions"; + +export const eventFormMachine = createMachine< + EventFormMachineContext, + FormHandlerEvents +>( + { + id: "eventFormMachine", + predictableActionArguments: true, + context: { + eventAsJson: {}, + originalSource: {}, + }, + on: { + [EventFormMachineTypes.SAVE_EVT]: { + target: EventFormMachineStates.EXIT, + }, + [EventFormMachineTypes.TOGGLE_FORM_EDITOR_EVT]: { + target: EventFormMachineStates.EXIT, + }, + [EventFormMachineTypes.RESET_CONFIRM_EVT]: { + actions: "resetForm", + }, + [EventFormMachineTypes.CONFIRM_NEW_EVENT]: { + actions: "resetFormToNewDefinition", + }, + }, + initial: EventFormMachineStates.IDLE, + states: { + [EventFormMachineStates.IDLE]: { + on: { + [EventFormMachineTypes.INPUT_CHANGE]: { + actions: ["handleInputChange"], + }, + [EventFormMachineTypes.ADD_ACTION]: { + actions: ["persistNewAction"], + }, + [EventFormMachineTypes.DELETE_ACTION]: { + actions: ["removeAction"], + }, + [EventFormMachineTypes.EDIT_ACTION]: { + actions: ["editAction"], + }, + }, + }, + [EventFormMachineStates.EXIT]: { + type: "final", + data: (context, event) => { + return { eventAsJson: context.eventAsJson, reason: event.type }; + }, + }, + }, + }, + { actions: actions as any }, +); diff --git a/ui-next/src/pages/definition/EventHandler/eventhandlers/FormComponent/state/types.ts b/ui-next/src/pages/definition/EventHandler/eventhandlers/FormComponent/state/types.ts new file mode 100644 index 0000000000..981938e46a --- /dev/null +++ b/ui-next/src/pages/definition/EventHandler/eventhandlers/FormComponent/state/types.ts @@ -0,0 +1,110 @@ +import { ConductorEvent } from "types/Events"; +export enum EventFormMachineTypes { + CHANGE_NAME_EVT = "CHANGE_NAME_EVT", + CHANGE_EVENT_EVT = "CHANGE_EVENT_EVT", + CHANGE_CONDITION_EVT = "CHANGE_CONDITION_EVT", + CHANGE_EVALUATOR_EVT = "CHANGE_EVALUATOR_EVT", + ADD_ACTION = "ADD_ACTION", + INPUT_CHANGE = "INPUT_CHANGE", + EDIT_ACTION = "EDIT_ACTION", + DELETE_ACTION = "DELETE_ACTION", + SAVE_EVT = "SAVE_EVT", + TOGGLE_FORM_EDITOR_EVT = "TOGGLE_FORM_EDITOR_EVT", + RESET_CONFIRM_EVT = "RESET_CONFIRM_EVT", + CONFIRM_NEW_EVENT = "CONFIRM_NEW_EVENT", +} + +export type InputChangeEvent = { + type: EventFormMachineTypes.INPUT_CHANGE; +}; + +export type AddEvent = { + type: EventFormMachineTypes.ADD_ACTION; +}; + +export type EditActionEvent = { + type: EventFormMachineTypes.EDIT_ACTION; +}; + +export type DeletActionEvent = { + type: EventFormMachineTypes.DELETE_ACTION; +}; + +export type SaveEvent = { + type: EventFormMachineTypes.SAVE_EVT; +}; + +export type ToggleFormModeEvent = { + type: EventFormMachineTypes.TOGGLE_FORM_EDITOR_EVT; +}; + +export type ResetConfirmEvent = { + type: EventFormMachineTypes.RESET_CONFIRM_EVT; +}; + +export type ConfirmNewEventEvent = { + type: EventFormMachineTypes.CONFIRM_NEW_EVENT; +}; + +export type FormHandlerEvents = + | InputChangeEvent + | AddEvent + | EditActionEvent + | DeletActionEvent + | SaveEvent + | ToggleFormModeEvent + | ResetConfirmEvent + | ConfirmNewEventEvent; + +export enum EventFormMachineStates { + IDLE = "idle", + EXIT = "exit", +} + +export enum QueueTypeSource { + KAFKA = "kafka", + AMQP = "amqp", + AZURE = "azure", + SQS = "sqs", +} +export const queueTypeLabel: { [key in QueueTypeSource]: string } = { + kafka: "kafka", + amqp: "amqp", + azure: "azure", + sqs: "sqs", +}; + +export enum Evaluator { + javascript = "javascript", + "value-param" = "value-param", +} +export const evaluatorLabel: { [key in Evaluator]: string } = { + javascript: "javascript", + "value-param": "value-param", +}; + +export enum Action { + COMPLETE_TASK = "complete_task", + TERMINATE_WORKFLOW = "terminate_workflow", + UPDATE_WORKFLOW_VARIABLES = "update_workflow_variables", + FAIL_TASK = "fail_task", + START_WORKFLOW = "start_workflow", +} + +export type AddActionEvent = { + type: EventFormMachineTypes.ADD_ACTION; + actionType: Action; +}; + +export const actionLabel = { + complete_task: "Complete Task", + terminate_workflow: "Terminate Workflow", + update_workflow_variables: "Update Variables", + fail_task: "Fail Task", + start_workflow: "Start Workflow", +} as { [key: string]: string }; + +export interface EventFormMachineContext { + eventAsJson: Partial; + originalSource: Partial; +} diff --git a/ui-next/src/pages/definition/EventHandler/eventhandlers/eventHandlerSchema.ts b/ui-next/src/pages/definition/EventHandler/eventhandlers/eventHandlerSchema.ts new file mode 100644 index 0000000000..0abc1dce39 --- /dev/null +++ b/ui-next/src/pages/definition/EventHandler/eventhandlers/eventHandlerSchema.ts @@ -0,0 +1,75 @@ +import { + CompleteActionType, + ConductorEvent, + FailActionType, + StartWorkflowAction, + TerminateWorkflowAction, + UpdateWorkFlowVariableType, +} from "types/Events"; + +// v2 +export const NEW_EVENT_HANDLER_TEMPLATE: Partial = { + name: "", + description: "", + event: "kafka:sampleConfig:sampleName", + evaluatorType: "javascript", + condition: "true", + actions: [ + { + action: "complete_task", + expandInlineJSON: false, + complete_task: { + workflowId: "${workflowId}", + taskRefName: "${taskReferenceName}", + }, + }, + ], +}; + +// TODO: Add schema definition for event handler + +export const COMPLETE_TASK_ACTION: CompleteActionType = { + action: "complete_task", + expandInlineJSON: false, + complete_task: { + workflowId: "${workflowId}", + taskRefName: "${taskReferenceName}", + }, +}; + +export const FAIL_TASK_ACTION: FailActionType = { + action: "fail_task", + expandInlineJSON: false, + fail_task: { + workflowId: "${workflowId}", + taskRefName: "${taskReferenceName}", + }, +}; + +export const UPDATE_VARIABLES_ACTION: UpdateWorkFlowVariableType = { + action: "update_workflow_variables", + expandInlineJSON: false, + update_workflow_variables: { + workflowId: "${targetWorkflowId}", + }, +}; + +export const START_WORKFLOW_ACTION: StartWorkflowAction = { + action: "start_workflow", + start_workflow: { + name: "sample_wf", + version: "", + correlationId: "", + idempotencyKey: "", + }, + expandInlineJSON: false, +}; + +export const TERMINATE_WORKFLOW_ACTION: TerminateWorkflowAction = { + action: "terminate_workflow", + expandInlineJSON: false, + terminate_workflow: { + workflowId: "", + terminationReason: "", + }, +}; diff --git a/ui-next/src/pages/definition/EventHandler/eventhandlers/state/actions.ts b/ui-next/src/pages/definition/EventHandler/eventhandlers/state/actions.ts new file mode 100644 index 0000000000..d00ba2319a --- /dev/null +++ b/ui-next/src/pages/definition/EventHandler/eventhandlers/state/actions.ts @@ -0,0 +1,153 @@ +import _omit from "lodash/omit"; +import _isEmpty from "lodash/isEmpty"; +import { assign, DoneInvokeEvent, forwardTo, send } from "xstate"; +import { cancel } from "xstate/lib/actions"; +import { NEW_EVENT_HANDLER_TEMPLATE } from "../eventHandlerSchema"; +import { + SaveEventHandlerMachineEventTypes, + SaveEventHandlerMachineContext, + EditEvent, + EditDebounceEvent, + UpdateEventHandlerEvent, + UpdateOriginalSourceEvent, + ShowErrorMessageEvent, + ClearErrorMessageEvent, +} from "./types"; +import { tryToJson, logger } from "utils"; + +export const editChanges = assign( + (_, { changes }) => { + const isValidJSON = !!tryToJson(changes); + if (!isValidJSON) { + logger.info("Json is broken"); + } + return { + editorChanges: changes, + couldNotParseJson: !isValidJSON, + }; + }, +); + +export const debounceEditEvent = send< + SaveEventHandlerMachineContext, + EditDebounceEvent +>( + (__, { changes }) => { + return { + type: SaveEventHandlerMachineEventTypes.EDIT_EVT, + changes, + }; + }, + { delay: 250, id: "debounce_edit_event" }, +); + +export const cancelDebounceEditChanges = cancel("debounce_edit_event"); + +export const updateEventHandlerName = assign( + ({ editorChanges }) => { + const eventHandlerJson = tryToJson<{ name: string }>(editorChanges); + return { + eventHandlerName: eventHandlerJson?.name, + }; + }, +); + +export const updateEventHandler = assign< + SaveEventHandlerMachineContext, + UpdateEventHandlerEvent +>((__, { data: eventHandler }) => { + const textVersion = JSON.stringify(eventHandler, null, 2); + + return { + editorChanges: textVersion, + originalSource: textVersion, + isNewEventHandler: !eventHandler?.name, + }; +}); + +export const updateOriginalSource = assign< + SaveEventHandlerMachineContext, + UpdateOriginalSourceEvent +>(({ editorChanges }, { data: eventHandler }) => { + const source = !_isEmpty(eventHandler) + ? JSON.stringify(eventHandler, null, 2) + : JSON.stringify(NEW_EVENT_HANDLER_TEMPLATE, null, 2); + + const newEditorChanges = + !_isEmpty(editorChanges) && editorChanges !== "" ? editorChanges : source; + + return { + originalSource: source, + editorChanges: newEditorChanges, + }; +}); + +export const revertToOriginalSource = assign( + ({ originalSource }) => { + return { + editorChanges: originalSource, + }; + }, +); + +export const resetToNewDefinition = assign( + () => { + const source = JSON.stringify(NEW_EVENT_HANDLER_TEMPLATE, null, 2); + return { + originalSource: source, + editorChanges: source, + }; + }, +); + +export const showErrorMessage = assign< + SaveEventHandlerMachineContext, + ShowErrorMessageEvent +>((_, errorEvent) => { + const message = + errorEvent?.data?.message || errorEvent?.data?.originalError?.message; + + return { + message, + }; +}); + +export const clearErrorMessage = assign< + SaveEventHandlerMachineContext, + ClearErrorMessageEvent +>(() => { + return { + message: "", + }; +}); + +export const forwardEventToFormMachine = forwardTo("eventFormMachine"); + +export const persistFormChanges = assign< + SaveEventHandlerMachineContext, + DoneInvokeEvent<{ + eventAsJson: { + name?: string; + event?: string; + evaluatorType?: string; + condition?: string; + actions?: []; + }; + reason: SaveEventHandlerMachineEventTypes; + }> +>((__context, { data }) => ({ + editorChanges: JSON.stringify(_omit(data.eventAsJson, "action"), null, 2), + reason: data.reason, +})); + +export const persistIsNewEventHandler = assign( + ({ eventHandlerName }) => ({ isNewEventHandler: !eventHandlerName }), +); + +export const sendSavedSuccessful = send({ + type: SaveEventHandlerMachineEventTypes.SAVED_SUCCESSFUL, +}); + +export const sendSavedCancelled = send({ + type: SaveEventHandlerMachineEventTypes.SAVED_CANCELLED, +}); diff --git a/ui-next/src/pages/definition/EventHandler/eventhandlers/state/guards.ts b/ui-next/src/pages/definition/EventHandler/eventhandlers/state/guards.ts new file mode 100644 index 0000000000..fd484fce61 --- /dev/null +++ b/ui-next/src/pages/definition/EventHandler/eventhandlers/state/guards.ts @@ -0,0 +1,25 @@ +import { logger } from "utils"; +import { SaveEventHandlerMachineContext } from "./types"; + +const maybeEventHandlerName = (eventHandlerAsString: string): string | null => { + try { + const eventHandler = JSON.parse(eventHandlerAsString); + const { name = null } = eventHandler; + return name; + } catch { + logger.debug("Event handler editor changes is not parsable"); + } + return null; +}; + +export const isNewOrNameChanged = ({ + isNewEventHandler, + originalSource, + editorChanges, +}: SaveEventHandlerMachineContext) => { + return ( + isNewEventHandler || + maybeEventHandlerName(editorChanges) !== + maybeEventHandlerName(originalSource) + ); +}; diff --git a/ui-next/src/pages/definition/EventHandler/eventhandlers/state/hook.ts b/ui-next/src/pages/definition/EventHandler/eventhandlers/state/hook.ts new file mode 100644 index 0000000000..2fb4145dc9 --- /dev/null +++ b/ui-next/src/pages/definition/EventHandler/eventhandlers/state/hook.ts @@ -0,0 +1,243 @@ +import { useInterpret, useSelector } from "@xstate/react"; +import { MessageContext } from "components/v1/layout/MessageContext"; +import _get from "lodash/get"; +import { useContext, useMemo } from "react"; +import { useLocation, useParams, Location } from "react-router"; +import { EVENT_HANDLERS_URL } from "utils/constants/route"; +import { usePushHistory } from "utils/hooks/usePushHistory"; +import { useAuthHeaders } from "utils/query"; +import { NEW_EVENT_HANDLER_TEMPLATE } from "../eventHandlerSchema"; +import { saveEventHandlerMachine } from "./machine"; +import { + SaveEventHandlerMachineEventTypes, + SaveEventHandlerStates, +} from "./types"; + +const isNewEventHandlerDef = (location: Location) => + location.pathname === EVENT_HANDLERS_URL.NEW; + +export const useEventHandlerDefinition = () => { + const authHeaders = useAuthHeaders(); + + const pushHistory = usePushHistory(); + const { setMessage } = useContext(MessageContext); + + const location = useLocation(); + const params = useParams(); + const isNewEventHandlerUrl = isNewEventHandlerDef(location); + const eventHandlerName = isNewEventHandlerUrl + ? "" + : decodeURIComponent(_get(params, "name") || ""); + + const service = useInterpret(saveEventHandlerMachine, { + ...(process.env.NODE_ENV === "development" ? { devTools: true } : {}), + context: { + authHeaders, + eventHandlerName, + originalSource: isNewEventHandlerUrl + ? JSON.stringify(NEW_EVENT_HANDLER_TEMPLATE, null, 2) + : "", + couldNotParseJson: false, + isNewEventHandler: isNewEventHandlerUrl, + }, + actions: { + pushToHistory: ({ eventHandlerName }) => { + pushHistory( + `${EVENT_HANDLERS_URL.BASE}/${encodeURIComponent(eventHandlerName)}`, + ); + }, + goBackToEventHandlersIndex: (_context) => { + pushHistory(EVENT_HANDLERS_URL.BASE); + }, + redirectToNew: () => { + pushHistory(EVENT_HANDLERS_URL.NEW); + }, + showSaveSuccessMessage: () => { + setMessage({ + text: "Event handler saved successfully.", + severity: "success", + }); + }, + }, + }); + + const handleEditChanges = (changes: string) => { + return service.send({ + type: SaveEventHandlerMachineEventTypes.EDIT_DEBOUNCE_EVT, + changes, + }); + }; + const isFormMode = useSelector(service, (state) => + state.matches([SaveEventHandlerStates.IDLE, SaveEventHandlerStates.FORM]), + ); + + const isEditorMode = useSelector(service, (state) => { + return ( + state.matches([ + SaveEventHandlerStates.IDLE, + SaveEventHandlerStates.EDITOR, + ]) || state.matches([SaveEventHandlerStates.CONFIRM_SAVE]) + ); + }); + + const isFetching = useSelector(service, (state) => { + return state.matches([ + SaveEventHandlerStates.FETCH_EVENT_HANDLER_DEFINITION, + ]); + }); + + const couldNotParseJson = useSelector( + service, + (state) => state.context.couldNotParseJson, + ); + + const handleSaveRequest = () => + service.send({ type: SaveEventHandlerMachineEventTypes.SAVE_EVT }); + + const handleConfirmSaveRequest = () => + service.send({ type: SaveEventHandlerMachineEventTypes.CONFIRM_SAVE_EVT }); + + const handleCancelRequest = () => + service.send({ type: SaveEventHandlerMachineEventTypes.CANCEL_SAVE_EVT }); + + const handleResetRequest = () => + service.send({ type: SaveEventHandlerMachineEventTypes.RESET_EVT }); + + const handleConfirmReset = () => + service.send({ type: SaveEventHandlerMachineEventTypes.RESET_CONFIRM_EVT }); + + const handleDeleteRequest = () => + service.send({ type: SaveEventHandlerMachineEventTypes.DELETE_EVT }); + + const handleConfirmDelete = () => + service.send({ + type: SaveEventHandlerMachineEventTypes.DELETE_CONFIRM_EVT, + }); + + const handleDefineNewEventHandler = () => { + service.send({ + type: SaveEventHandlerMachineEventTypes.NEW_EVENT_HANDLER_REQUEST, + }); + }; + + const handleConfirmNewEventHandler = () => { + service.send({ + type: SaveEventHandlerMachineEventTypes.CONFIRM_NEW_EVENT, + }); + }; + + const handleBackToIdle = () => { + service.send({ + type: SaveEventHandlerMachineEventTypes.BACK_TO_IDLE, + }); + }; + + const handleClearErrorMessage = () => { + service.send({ + type: SaveEventHandlerMachineEventTypes.CLEAR_ERROR_MESSAGE, + }); + }; + + const originalSource = useSelector( + service, + (state) => state.context.originalSource, + ); + + const editorChanges = useSelector( + service, + (state) => state.context.editorChanges, + ); + + const isNewEventHandler = useSelector( + service, + (state) => state.context.isNewEventHandler, + ); + + const message = useSelector(service, (state) => state.context.message); + // const errors = useSelector(service, (state) => state.context.errors); + + const isIdle = useSelector(service, (state) => state.matches("idle")); + + const isConfirmSave = useSelector(service, (state) => + state.matches("confirmSave"), + ); + + const isSaving = useSelector( + service, + (state) => + state.matches("createEventHandler") || + state.matches("updateEventHandler"), + ); + + const isConfirmReset = useSelector(service, (state) => + ["idle.form.confirmReset", "idle.editor.confirmReset"].some(state.matches), + ); + + const isConfirmDelete = useSelector(service, (state) => + ["idle.form.confirmDelete", "idle.editor.confirmDelete"].some( + state.matches, + ), + ); + + const isConfirmNew = useSelector(service, (state) => + ["idle.form.confirmNew", "idle.editor.confirmNew"].some(state.matches), + ); + + const isUpdatingToNewChanges = useSelector(service, (state) => + state.matches("refetchEventHandlerChanges"), + ); + + const madeChanges = useMemo( + () => + editorChanges !== "" && + (editorChanges !== originalSource || isNewEventHandler), + [editorChanges, originalSource, isNewEventHandler], + ); + + const toggleFormMode = () => { + service.send({ + type: SaveEventHandlerMachineEventTypes.TOGGLE_FORM_EDITOR_EVT, + isEditorMode: !isEditorMode, + }); + }; + + return [ + { + handleDeleteRequest, + handleConfirmDelete, + handleConfirmReset, + handleResetRequest, + handleCancelRequest, + handleConfirmSaveRequest, + handleSaveRequest, + handleEditChanges, + handleDefineNewEventHandler, + handleConfirmNewEventHandler, + handleBackToIdle, + handleClearErrorMessage, + toggleFormMode, + service, + }, + { + isNewEventHandler, + eventHandlerName, + originalSource, + editorChanges, + isConfirmReset, + isConfirmDelete, + isConfirmNew, + // message, + // errors, + madeChanges, + isUpdatingToNewChanges, + isConfirmSave, + isSaving, + isIdle, + message, + isFormMode, + isEditorMode, + couldNotParseJson, + isFetching, + }, + ] as const; +}; diff --git a/ui-next/src/pages/definition/EventHandler/eventhandlers/state/index.ts b/ui-next/src/pages/definition/EventHandler/eventhandlers/state/index.ts new file mode 100644 index 0000000000..79fd82215f --- /dev/null +++ b/ui-next/src/pages/definition/EventHandler/eventhandlers/state/index.ts @@ -0,0 +1,2 @@ +export * from "./machine"; +export * from "./types"; diff --git a/ui-next/src/pages/definition/EventHandler/eventhandlers/state/machine.ts b/ui-next/src/pages/definition/EventHandler/eventhandlers/state/machine.ts new file mode 100644 index 0000000000..237f63ef34 --- /dev/null +++ b/ui-next/src/pages/definition/EventHandler/eventhandlers/state/machine.ts @@ -0,0 +1,315 @@ +import { eventFormMachine } from "../FormComponent/state/machine"; +import { createMachine } from "xstate"; +import { + SaveEventHandlerMachineEventTypes, + SaveEventHandlerEvents, + SaveEventHandlerMachineContext, + SaveEventHandlerStates, +} from "./types"; + +import * as actions from "./actions"; +import * as guards from "./guards"; +import * as services from "./services"; +import { tryToJson } from "utils"; + +export const saveEventHandlerMachine = createMachine< + SaveEventHandlerMachineContext, + SaveEventHandlerEvents +>( + { + id: "saveEventHandlerMachine", + predictableActionArguments: true, + initial: SaveEventHandlerStates.FETCH_EVENT_HANDLER_DEFINITION, + context: { + originalSource: "", + editorChanges: "", + isNewEventHandler: false, + eventHandlerName: "", + authHeaders: {}, + message: "", + couldNotParseJson: false, + }, + // Handle SAVED_SUCCESSFUL and SAVED_CANCELLED at the top level so they can be received from any state + on: { + [SaveEventHandlerMachineEventTypes.SAVED_SUCCESSFUL]: { + actions: [], // No-op, just to receive the event so it can be detected + }, + [SaveEventHandlerMachineEventTypes.SAVED_CANCELLED]: { + actions: [], // No-op, just to receive the event so it can be detected + }, + }, + states: { + [SaveEventHandlerStates.IDLE]: { + on: { + [SaveEventHandlerMachineEventTypes.SAVE_EVT]: { + target: SaveEventHandlerStates.CONFIRM_SAVE, + }, + [SaveEventHandlerMachineEventTypes.EDIT_EVT]: { + actions: ["editChanges"], + }, + [SaveEventHandlerMachineEventTypes.CLEAR_ERROR_MESSAGE]: { + target: SaveEventHandlerStates.IDLE, + actions: ["clearErrorMessage"], + }, + }, + initial: SaveEventHandlerStates.FORM, + states: { + [SaveEventHandlerStates.EDITOR]: { + on: { + [SaveEventHandlerMachineEventTypes.TOGGLE_FORM_EDITOR_EVT]: { + target: SaveEventHandlerStates.FORM, + }, + [SaveEventHandlerMachineEventTypes.RESET_EVT]: { + target: `.${SaveEventHandlerStates.CONFIRM_RESET}`, + }, + [SaveEventHandlerMachineEventTypes.DELETE_EVT]: { + target: `.${SaveEventHandlerStates.CONFIRM_DELETE}`, + }, + [SaveEventHandlerMachineEventTypes.NEW_EVENT_HANDLER_REQUEST]: { + target: `.${SaveEventHandlerStates.CONFIRM_NEW}`, + }, + [SaveEventHandlerMachineEventTypes.EDIT_DEBOUNCE_EVT]: { + actions: ["cancelDebounceEditChanges", "debounceEditEvent"], + }, + }, + initial: SaveEventHandlerStates.IDLE, + states: { + [SaveEventHandlerStates.IDLE]: {}, + + [SaveEventHandlerStates.CONFIRM_RESET]: { + on: { + [SaveEventHandlerMachineEventTypes.RESET_CONFIRM_EVT]: { + actions: ["revertToOriginalSource"], + target: SaveEventHandlerStates.IDLE, + }, + [SaveEventHandlerMachineEventTypes.BACK_TO_IDLE]: { + target: SaveEventHandlerStates.IDLE, + }, + }, + }, + [SaveEventHandlerStates.CONFIRM_DELETE]: { + on: { + [SaveEventHandlerMachineEventTypes.DELETE_CONFIRM_EVT]: { + target: SaveEventHandlerStates.DELETE_EVENT_HANDLER, + }, + [SaveEventHandlerMachineEventTypes.BACK_TO_IDLE]: { + target: SaveEventHandlerStates.IDLE, + }, + }, + }, + [SaveEventHandlerStates.DELETE_EVENT_HANDLER]: { + invoke: { + src: "deleteEventHandler", + id: "delete-event-handler", + onDone: { + target: SaveEventHandlerStates.IDLE, + actions: ["goBackToEventHandlersIndex"], + }, + onError: { + target: SaveEventHandlerStates.IDLE, + actions: ["showErrorMessage"], + }, + }, + }, + [SaveEventHandlerStates.CONFIRM_NEW]: { + on: { + [SaveEventHandlerMachineEventTypes.CONFIRM_NEW_EVENT]: { + target: SaveEventHandlerStates.IDLE, + actions: ["resetToNewDefinition", "redirectToNew"], + }, + [SaveEventHandlerMachineEventTypes.BACK_TO_IDLE]: { + target: SaveEventHandlerStates.IDLE, + }, + }, + }, + }, + }, + [SaveEventHandlerStates.FORM]: { + on: { + [SaveEventHandlerMachineEventTypes.TOGGLE_FORM_EDITOR_EVT]: { + actions: "forwardEventToFormMachine", + }, + [SaveEventHandlerMachineEventTypes.SAVE_EVT]: { + actions: "forwardEventToFormMachine", + }, + [SaveEventHandlerMachineEventTypes.RESET_EVT]: { + target: `.${SaveEventHandlerStates.CONFIRM_RESET}`, + }, + [SaveEventHandlerMachineEventTypes.DELETE_EVT]: { + target: `.${SaveEventHandlerStates.CONFIRM_DELETE}`, + }, + [SaveEventHandlerMachineEventTypes.NEW_EVENT_HANDLER_REQUEST]: { + target: `.${SaveEventHandlerStates.CONFIRM_NEW}`, + }, + }, + invoke: { + id: "eventFormMachine", + src: eventFormMachine, + data: (context: SaveEventHandlerMachineContext) => { + const eventAsJson = tryToJson(context.editorChanges); + const originalSource = tryToJson(context.originalSource); + return { + eventAsJson, + originalSource, + }; + }, + onDone: [ + { + cond: (__context, { data }) => + data.reason === SaveEventHandlerMachineEventTypes.SAVE_EVT, + target: `#saveEventHandlerMachine.${SaveEventHandlerStates.CONFIRM_SAVE}`, + actions: "persistFormChanges", + }, + { + target: SaveEventHandlerStates.EDITOR, + actions: "persistFormChanges", + }, + ], + }, + initial: SaveEventHandlerStates.IDLE, + states: { + [SaveEventHandlerStates.IDLE]: {}, + [SaveEventHandlerStates.CONFIRM_RESET]: { + on: { + [SaveEventHandlerMachineEventTypes.RESET_CONFIRM_EVT]: { + actions: "forwardEventToFormMachine", + target: SaveEventHandlerStates.IDLE, + }, + [SaveEventHandlerMachineEventTypes.BACK_TO_IDLE]: { + target: SaveEventHandlerStates.IDLE, + }, + }, + }, + [SaveEventHandlerStates.CONFIRM_DELETE]: { + on: { + [SaveEventHandlerMachineEventTypes.DELETE_CONFIRM_EVT]: { + target: SaveEventHandlerStates.DELETE_EVENT_HANDLER, + }, + [SaveEventHandlerMachineEventTypes.BACK_TO_IDLE]: { + target: SaveEventHandlerStates.IDLE, + }, + }, + }, + [SaveEventHandlerStates.DELETE_EVENT_HANDLER]: { + invoke: { + src: "deleteEventHandler", + id: "delete-event-handler", + onDone: { + target: SaveEventHandlerStates.IDLE, + actions: ["goBackToEventHandlersIndex"], + }, + onError: { + target: SaveEventHandlerStates.IDLE, + actions: ["showErrorMessage"], + }, + }, + }, + [SaveEventHandlerStates.CONFIRM_NEW]: { + on: { + [SaveEventHandlerMachineEventTypes.CONFIRM_NEW_EVENT]: { + actions: ["forwardEventToFormMachine", "redirectToNew"], + target: SaveEventHandlerStates.IDLE, + }, + [SaveEventHandlerMachineEventTypes.BACK_TO_IDLE]: { + target: SaveEventHandlerStates.IDLE, + }, + }, + }, + }, + }, + }, + }, + [SaveEventHandlerStates.FETCH_EVENT_HANDLER_DEFINITION]: { + invoke: { + src: "fetchEventHandler", + onDone: { + actions: ["updateEventHandler", "updateOriginalSource"], + target: SaveEventHandlerStates.IDLE, + }, + onError: { + actions: ["showErrorMessage"], + }, + }, + }, + [SaveEventHandlerStates.CONFIRM_SAVE]: { + on: { + [SaveEventHandlerMachineEventTypes.CONFIRM_SAVE_EVT]: [ + { + target: SaveEventHandlerStates.CREATE_EVENT_HANDLER, + cond: "isNewOrNameChanged", + }, + { target: SaveEventHandlerStates.UPDATE_EVENT_HANDLER }, + ], + [SaveEventHandlerMachineEventTypes.CANCEL_SAVE_EVT]: { + target: SaveEventHandlerStates.IDLE, + actions: ["sendSavedCancelled"], + }, + [SaveEventHandlerMachineEventTypes.EDIT_EVT]: { + actions: ["editChanges"], + }, + [SaveEventHandlerMachineEventTypes.EDIT_DEBOUNCE_EVT]: { + actions: ["cancelDebounceEditChanges", "debounceEditEvent"], + }, + }, + }, + + [SaveEventHandlerStates.CREATE_EVENT_HANDLER]: { + invoke: { + src: "createEventHandler", + id: "create-event-handler", + onDone: { + actions: [ + "updateEventHandlerName", + "pushToHistory", + "showSaveSuccessMessage", + "persistIsNewEventHandler", + "sendSavedSuccessful", + ], + target: SaveEventHandlerStates.FETCH_EVENT_HANDLER_DEFINITION, + }, + onError: [ + { + target: SaveEventHandlerStates.IDLE, + actions: ["showErrorMessage"], + }, + ], + }, + }, + [SaveEventHandlerStates.UPDATE_EVENT_HANDLER]: { + invoke: { + src: "updateEventHandler", + id: "update-event-handler", + onDone: { + actions: [ + "updateEventHandlerName", + "showSaveSuccessMessage", + "sendSavedSuccessful", + ], + target: SaveEventHandlerStates.FETCH_EVENT_HANDLER_DEFINITION, + }, + onError: { + target: SaveEventHandlerStates.IDLE, + actions: ["showErrorMessage"], + }, + }, + }, + [SaveEventHandlerStates.SAVED_SUCCESSFULLY]: { + type: "final", + data: ({ editorChanges, isNewEventHandler, eventHandlerName }) => ({ + saved: true, + editorChanges, + isNewEventHandler, + eventHandlerName, + }), + }, + [SaveEventHandlerStates.SAVED_CANCELLED]: { + type: "final", + data: ({ editorChanges }) => ({ + saved: false, + editorChanges, + }), + }, + }, + }, + { actions: actions as any, guards, services }, +); diff --git a/ui-next/src/pages/definition/EventHandler/eventhandlers/state/services.ts b/ui-next/src/pages/definition/EventHandler/eventhandlers/state/services.ts new file mode 100644 index 0000000000..0ee8da35b7 --- /dev/null +++ b/ui-next/src/pages/definition/EventHandler/eventhandlers/state/services.ts @@ -0,0 +1,108 @@ +import _isEmpty from "lodash/isEmpty"; +import { fetchWithContext } from "plugins/fetch"; +import { SaveEventHandlerMachineContext } from "./types"; +import { queryClient } from "../../../../../queryClient"; +import { fetchContextNonHook } from "plugins/fetch"; +import { tryFunc } from "utils"; +import { NEW_EVENT_HANDLER_TEMPLATE } from "../eventHandlerSchema"; + +const fetchContext = fetchContextNonHook(); + +export const createEventHandler = async ( + { editorChanges, authHeaders }: SaveEventHandlerMachineContext, + __: any, +) => { + return tryFunc({ + fn: async () => { + return await fetchWithContext( + "/event", + {}, + { + method: "POST", + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + body: editorChanges, + }, + ); + }, + }); +}; + +export const updateEventHandler = async ( + { editorChanges, authHeaders }: SaveEventHandlerMachineContext, + __: any, +) => { + return tryFunc({ + fn: async () => { + return await fetchWithContext( + "/event", + {}, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + body: editorChanges, + }, + ); + }, + }); +}; + +export const fetchEventHandler = async ( + { + authHeaders, + eventHandlerName, + isNewEventHandler, + }: SaveEventHandlerMachineContext, + __: any, +) => { + // OSS Conductor doesn't have a /event/handler/{name} endpoint + // We need to fetch all event handlers and filter by name + const path = "/event"; + + return tryFunc({ + fn: async () => { + if (isNewEventHandler) { + return NEW_EVENT_HANDLER_TEMPLATE; + } + + const allHandlers = await queryClient.fetchQuery( + [path, eventHandlerName], + () => fetchWithContext(path, fetchContext, { headers: authHeaders }), + ); + + // Find the event handler by name + const result = Array.isArray(allHandlers) + ? allHandlers.find((handler: any) => handler.name === eventHandlerName) + : null; + + if (_isEmpty(result)) { + return Promise.reject({ message: "Event handler not found" }); + } + + return result; + }, + customError: { message: "Event handler not found" }, + }); +}; + +export const deleteEventHandler = async ( + { eventHandlerName, authHeaders }: SaveEventHandlerMachineContext, + __: any, +) => { + return tryFunc({ + fn: async () => { + const path = `/event/${encodeURIComponent(eventHandlerName)}`; + const result = await fetchWithContext(path, fetchContext, { + method: "DELETE", + headers: authHeaders, + }); + + return result; + }, + }); +}; diff --git a/ui-next/src/pages/definition/EventHandler/eventhandlers/state/types.ts b/ui-next/src/pages/definition/EventHandler/eventhandlers/state/types.ts new file mode 100644 index 0000000000..d0edd9b325 --- /dev/null +++ b/ui-next/src/pages/definition/EventHandler/eventhandlers/state/types.ts @@ -0,0 +1,153 @@ +import { DoneInvokeEvent } from "xstate"; + +export enum SaveEventHandlerMachineEventTypes { + SAVE_EVT = "SAVE_EVT", + CONFIRM_SAVE_EVT = "CONFIRM_SAVE", + CANCEL_SAVE_EVT = "CANCEL_SAVE", + EDIT_EVT = "EDIT_EVT", + EDIT_DEBOUNCE_EVT = "CANCEL_DEBOUNCE", + RESET_EVT = "RESET_EVT", + RESET_CONFIRM_EVT = "RESET_CONFIRM_EVT", + DELETE_EVT = "DELETE_EVT", + DELETE_CONFIRM_EVT = "DELETE_CONFIRM_EVT", + UPDATE_EVENTHANDLER_EVT = "UPDATE_EVENTHANDLER_EVT", + UPDATE_ORIGINAL_SOURCE_EVT = "UPDATE_ORIGINAL_SOURCE_EVT", + NEW_EVENT_HANDLER_REQUEST = "NEW_EVENT_HANDLER_REQUEST", + CONFIRM_NEW_EVENT = "CONFIRM_NEW_EVENT", + BACK_TO_IDLE = "BACK_TO_IDLE", + SHOW_ERROR_MESSAGE = "SHOW_ERROR_MESSAGE", + CLEAR_ERROR_MESSAGE = "CLEAR_ERROR_MESSAGE", + TOGGLE_FORM_EDITOR_EVT = "TOGGLE_FORM_EDITOR_EVT", + SAVED_SUCCESSFUL = "SAVED_SUCCESSFUL", + SAVED_CANCELLED = "SAVED_CANCELLED", +} + +export type ResetEvent = { + type: SaveEventHandlerMachineEventTypes.RESET_EVT; +}; + +export type ResetConfirmEvent = { + type: SaveEventHandlerMachineEventTypes.RESET_CONFIRM_EVT; +}; + +export type DeleteEvent = { + type: SaveEventHandlerMachineEventTypes.DELETE_EVT; +}; + +export type DeleteConfirmEvent = { + type: SaveEventHandlerMachineEventTypes.DELETE_CONFIRM_EVT; +}; + +export type SaveEvent = { + type: SaveEventHandlerMachineEventTypes.SAVE_EVT; +}; + +export type ConfirmSaveEvent = { + type: SaveEventHandlerMachineEventTypes.CONFIRM_SAVE_EVT; +}; + +export type CancelSaveEvent = { + type: SaveEventHandlerMachineEventTypes.CANCEL_SAVE_EVT; +}; + +export type EditEvent = { + type: SaveEventHandlerMachineEventTypes.EDIT_EVT; + changes: string; +}; + +export type EditDebounceEvent = { + type: SaveEventHandlerMachineEventTypes.EDIT_DEBOUNCE_EVT; + changes: string; +}; + +export type UpdateEventHandlerEvent = { + type: SaveEventHandlerMachineEventTypes.UPDATE_EVENTHANDLER_EVT; + data: any; +}; + +export type UpdateOriginalSourceEvent = { + type: SaveEventHandlerMachineEventTypes.UPDATE_ORIGINAL_SOURCE_EVT; + data: any; +}; + +export type NewEventHandlerRequestEvent = { + type: SaveEventHandlerMachineEventTypes.NEW_EVENT_HANDLER_REQUEST; +}; + +export type ConfirmNewEventEvent = { + type: SaveEventHandlerMachineEventTypes.CONFIRM_NEW_EVENT; +}; + +export type BackToIdleEvent = { + type: SaveEventHandlerMachineEventTypes.BACK_TO_IDLE; +}; + +export type ShowErrorMessageEvent = { + type: SaveEventHandlerMachineEventTypes.SHOW_ERROR_MESSAGE; + data: Record; +}; + +export type ClearErrorMessageEvent = { + type: SaveEventHandlerMachineEventTypes.CLEAR_ERROR_MESSAGE; +}; + +export type ToggleFormModeEvent = { + type: SaveEventHandlerMachineEventTypes.TOGGLE_FORM_EDITOR_EVT; + isEditorMode: boolean; +}; + +export type SavedSuccessfulEvent = { + type: SaveEventHandlerMachineEventTypes.SAVED_SUCCESSFUL; +}; + +export type SavedCancelledEvent = { + type: SaveEventHandlerMachineEventTypes.SAVED_CANCELLED; +}; + +export type SaveEventHandlerEvents = + | SaveEvent + | ConfirmSaveEvent + | CancelSaveEvent + | EditEvent + | EditDebounceEvent + | DoneInvokeEvent + | ResetEvent + | ResetConfirmEvent + | DeleteEvent + | DeleteConfirmEvent + | UpdateEventHandlerEvent + | UpdateOriginalSourceEvent + | NewEventHandlerRequestEvent + | ConfirmNewEventEvent + | BackToIdleEvent + | ShowErrorMessageEvent + | ClearErrorMessageEvent + | ToggleFormModeEvent + | SavedSuccessfulEvent + | SavedCancelledEvent; + +export interface SaveEventHandlerMachineContext { + editorChanges: string; + originalSource: string; + isNewEventHandler: boolean; + eventHandlerName: string; + authHeaders: Record; + message: string; + couldNotParseJson: boolean; +} + +export enum SaveEventHandlerStates { + IDLE = "idle", + EDITOR = "editor", + FORM = "form", + FETCH_EVENT_HANDLER_DEFINITION = "fetchEventHandlerDefinition", + CONFIRM_SAVE = "confirmSave", + CREATE_EVENT_HANDLER = "createEventHandler", + UPDATE_EVENT_HANDLER = "updateEventHandler", + SAVED_SUCCESSFULLY = "savedSuccessfully", + SAVED_CANCELLED = "savedCancelled", + CONFIRM_RESET = "confirmReset", + CONFIRM_DELETE = "confirmDelete", + CONFIRM_NEW = "confirmNew", + DELETE_EVENT_HANDLER = "deleteEventHandler", +} diff --git a/ui-next/src/pages/definition/EventHandler/index.ts b/ui-next/src/pages/definition/EventHandler/index.ts new file mode 100644 index 0000000000..6ac204e563 --- /dev/null +++ b/ui-next/src/pages/definition/EventHandler/index.ts @@ -0,0 +1,3 @@ +import EventHandlerDefinition from "./EventHandler"; + +export { EventHandlerDefinition }; diff --git a/ui-next/src/pages/definition/GraphPanel.jsx b/ui-next/src/pages/definition/GraphPanel.jsx new file mode 100644 index 0000000000..3d1260ded8 --- /dev/null +++ b/ui-next/src/pages/definition/GraphPanel.jsx @@ -0,0 +1,128 @@ +import { useCallback, useState } from "react"; +import { Box } from "@mui/material"; +import { useSelector } from "@xstate/react"; +import { Flow } from "components/flow/Flow"; +import { useLocalCopyMachine } from "./ConfirmLocalCopyDialog/state/hook"; +import { useMemo } from "react"; +import ProgressIcon from "./progressicons"; +import { X } from "@phosphor-icons/react"; +import { DefinitionMachineEventTypes } from "pages/definition/state/types"; +import MuiAlert from "components/MuiAlert"; +import { usePanelChanges } from "pages/definition/state/usePanelChanges"; +import { selectIsOpenedEdge } from "components/flow/state/selectors"; +import AddTaskSidebar from "components/flow/components/RichAddTaskMenu/AddTaskSidebar"; + +const GraphPanel = ({ definitionActor }) => { + const { leftPanelExpanded, setLeftPanelExpanded } = + usePanelChanges(definitionActor); + const localCopyMessage = useSelector( + definitionActor, + (state) => state.context.localCopyMessage, + ); + const flowActor = definitionActor.children?.get("flowMachine"); + const [isHovered, setIsHovered] = useState(false); + + const openedEdge = useSelector(flowActor, selectIsOpenedEdge); + + const richAddTaskMenuActor = flowActor?.children.get( + "richAddTaskMenuMachine", + ); + + const menuType = useSelector(richAddTaskMenuActor || flowActor, (state) => + richAddTaskMenuActor ? state.context.menuType : undefined, + ); + + const [{ handleRemoveLocalCopyMessage }] = + useLocalCopyMachine(definitionActor); + const handleResetRequest = useCallback(() => { + definitionActor.send({ type: DefinitionMachineEventTypes.RESET_EVT }); + }, [definitionActor]); + + const linkStyle = useMemo( + () => ({ + cursor: "pointer", + color: isHovered ? "#13599e" : "#1976d2", + padding: "0 3px", + }), + [isHovered], + ); + const localCopyAlert = useMemo( + () => ( + + ), + [handleResetRequest, localCopyMessage, linkStyle], + ); + return ( + + {localCopyMessage ? ( + + + + + handleRemoveLocalCopyMessage()} + /> + + } + > + {localCopyAlert} + + ) : null} + + {flowActor && + (openedEdge && menuType === "advanced" ? ( + + + + + + + + ) : ( + + ))} + + ); +}; + +export default GraphPanel; diff --git a/ui-next/src/pages/definition/ImportSuccessfulDialog.tsx b/ui-next/src/pages/definition/ImportSuccessfulDialog.tsx new file mode 100644 index 0000000000..9fe517ebd7 --- /dev/null +++ b/ui-next/src/pages/definition/ImportSuccessfulDialog.tsx @@ -0,0 +1,52 @@ +import ConfirmChoiceDialog from "components/ConfirmChoiceDialog"; +import { openInNewTab } from "utils/helpers"; + +const ImportSuccessfulDialog = ({ + workflowName, + blogUrl, + hideModal, +}: { + workflowName: string; + blogUrl?: string; + hideModal: () => void; +}) => { + const handleConfirmationValue = (confirmed: boolean) => { + if (confirmed) { + hideModal(); + } + }; + + return ( + + You have successfully imported {workflowName} into your cluster and + you can start running this. + {blogUrl && ( +
    + Refer to this{" "} + openInNewTab(blogUrl)} + style={{ + cursor: "pointer", + color: "#1976d2", + fontWeight: "500", + }} + > + article + {" "} + to understand more details about this workflow and how to use it. +
    + )} + + } + id="workflow-import-successful-dialog" + header={"Nice work"} + confirmBtnLabel="Close" + hideCancelBtn + /> + ); +}; + +export default ImportSuccessfulDialog; diff --git a/ui-next/src/pages/definition/PromptIfChanges.tsx b/ui-next/src/pages/definition/PromptIfChanges.tsx new file mode 100644 index 0000000000..50b9fb16a3 --- /dev/null +++ b/ui-next/src/pages/definition/PromptIfChanges.tsx @@ -0,0 +1,118 @@ +import fastDeepEqual from "fast-deep-equal"; +import { FunctionComponent, useCallback } from "react"; +import BlockNavigationWithConfirmation from "shared/BlockNavigationWithConfirmation"; +import { useSaveProtection } from "shared/useSaveProtection"; +import { ActorRef } from "xstate"; +import { SaveWorkflowMachineEventTypes } from "./confirmSave/state/types"; +import { + DefinitionMachineEventTypes, + WorkflowDefinitionEvents, +} from "./state/types"; +import { useWorkflowChanges } from "./state/useMadeChanges"; +import { useAgentContext } from "components/agent/AgentContext"; +export interface HeaderActionButtonsProps { + definitionActor: ActorRef; +} + +export const PromptIfChanges: FunctionComponent = ({ + definitionActor: service, +}) => { + const { cancelStream, clearMessages } = useAgentContext(); + const { workflowChanges, currentWf } = useWorkflowChanges(service); + const noFormChanges = currentWf + ? fastDeepEqual(workflowChanges, currentWf) + : true; + + // Simple validation check for common issues + const checkHasErrors = (workflowToCheck: typeof workflowChanges) => { + if (!workflowToCheck) { + return false; + } + + // Check for common validation issues + const issues = []; + + // Check if name is missing or empty + if (!workflowToCheck.name || workflowToCheck.name.trim() === "") { + issues.push("Missing workflow name"); + } + + // Check if description is missing or empty + if ( + !workflowToCheck.description || + workflowToCheck.description.trim() === "" + ) { + issues.push("Missing workflow description"); + } + + // Check if tasks array exists and has items + if (!workflowToCheck.tasks || workflowToCheck.tasks.length === 0) { + issues.push("No tasks defined"); + } + + // Check for tasks with missing required fields + if (workflowToCheck.tasks) { + workflowToCheck.tasks.forEach( + ( + task: { name?: string; taskReferenceName?: string; type?: string }, + index: number, + ) => { + if (!task.name || task.name.trim() === "") { + issues.push(`Task ${index + 1} is missing a name`); + } + if (!task.taskReferenceName || task.taskReferenceName.trim() === "") { + issues.push(`Task ${index + 1} is missing a taskReferenceName`); + } + if (!task.type || task.type.trim() === "") { + issues.push(`Task ${index + 1} is missing a type`); + } + }, + ); + } + + return issues.length > 0; + }; + + const { showPrompt, successfulSave, hasErrors, handleSave } = + useSaveProtection({ + actor: service, + noFormChanges, + isSaveInProgress: (state) => state.hasTag?.("saveRequest") ?? false, + hasErrors: () => { + const workflowToCheck = workflowChanges || currentWf; + return checkHasErrors(workflowToCheck); + }, + detectSaveSuccessFromEvent: (eventType) => { + if (eventType === SaveWorkflowMachineEventTypes.SAVED_SUCCESSFUL) { + return true; + } else if ( + eventType === SaveWorkflowMachineEventTypes.SAVED_CANCELLED + ) { + return false; + } + return undefined; + }, + handleSaveAction: (actor) => { + actor.send({ type: DefinitionMachineEventTypes.SAVE_EVT }); + }, + }); + + const handleDiscard = useCallback(() => { + // Cancel any ongoing stream + cancelStream(); + // Clear assistant chat messages + clearMessages(); + }, [cancelStream, clearMessages]); + + return ( + + ); +}; diff --git a/ui-next/src/pages/definition/RunWorkflow/RunWorkflowForm.tsx b/ui-next/src/pages/definition/RunWorkflow/RunWorkflowForm.tsx new file mode 100644 index 0000000000..abe526043d --- /dev/null +++ b/ui-next/src/pages/definition/RunWorkflow/RunWorkflowForm.tsx @@ -0,0 +1,158 @@ +import { Box, Grid } from "@mui/material"; +import { Button } from "components"; +import Paper from "components/Paper"; +import { SnackbarMessage } from "components/SnackbarMessage"; +import { ConductorCodeBlockInput } from "components/v1/ConductorCodeBlockInput"; +import ConductorInput from "components/v1/ConductorInput"; +import ResetIcon from "components/v1/icons/ResetIcon"; +import IdempotencyForm from "pages/runWorkflow/IdempotencyForm"; +import { editor, type EditorOptions } from "shared/editor"; +import { SMALL_EDITOR_DEFAULT_OPTIONS } from "utils/constants"; +import { useLocalStorage } from "utils/localstorage"; +import { ActorRef } from "xstate"; +import { WorkflowDefinitionEvents } from "../state"; +import { RunWorkflowHistoryTable } from "./RunWorkflowHistoryTable"; +import { + RunMachineEvents, + RunWorkflowParamType, + useRunTabActor, +} from "./state"; + +interface RunWorkFlowFormProps { + runTabActor: ActorRef; + workflowDefinitionActor: ActorRef; +} + +const additionalEditorOptions: EditorOptions = { + scrollBeyondLastLine: false, + wrappingStrategy: "advanced", + lightbulb: { enabled: editor.ShowLightbulbIconMode.On }, + quickSuggestions: true, + lineNumbers: "on", + wordWrap: "on", + glyphMargin: false, + folding: false, + lineDecorationsWidth: 10, + lineNumbersMinChars: 0, + renderLineHighlight: "none", + hideCursorInOverviewRuler: false, + overviewRulerBorder: false, + automaticLayout: true, // Important +}; + +export const RunWorkFlowForm = ({ runTabActor }: RunWorkFlowFormProps) => { + const [ + { + currentWf, + input, + correlationId, + taskToDomain, + + popoverMessage, + idempotencyKey, + idempotencyStrategy, + }, + { + handleChangeInputParams, + handleChangeCorrelationId, + handleChangeTasksToDomain, + handleClearForm, + + handlePopoverMessage, + handleFillAllFields, + handleChangeIdempotencyValues, + }, + ] = useRunTabActor(runTabActor); + + const [workflowHistory, setWorkflowHistory] = useLocalStorage( + "workflowHistory", + [], + ); + const handlefillReRunWfFields = (data: RunWorkflowParamType) => { + const payload = { + correlationId: data.correlationId, + input: JSON.stringify(data.input, null, 2) ?? "", + taskToDomain: JSON.stringify(data.taskToDomain, null, 2) ?? "", + idempotencyKey: data.idempotencyKey, + idempotencyStrategy: data.idempotencyStrategy, + }; + handleFillAllFields(payload); + }; + + return ( + + + {popoverMessage && ( + handlePopoverMessage(null)} + /> + )} + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/ui-next/src/pages/definition/RunWorkflow/RunWorkflowHistoryTable.tsx b/ui-next/src/pages/definition/RunWorkflow/RunWorkflowHistoryTable.tsx new file mode 100644 index 0000000000..cdb4805d74 --- /dev/null +++ b/ui-next/src/pages/definition/RunWorkflow/RunWorkflowHistoryTable.tsx @@ -0,0 +1,192 @@ +import { Box, Link as ClickableLink, Tooltip } from "@mui/material"; +import IconButton from "@mui/material/IconButton"; +import { + Link as LinkIcon, + ArrowCounterClockwise as Replay, + Trash, +} from "@phosphor-icons/react"; +import { Button, DataTable, Paper } from "components"; +import ConfirmChoiceDialog from "components/ConfirmChoiceDialog"; +import _difference from "lodash/difference"; +import _isEmpty from "lodash/isEmpty"; +import _isEqual from "lodash/isEqual"; +import { FunctionComponent, useMemo, useState } from "react"; +import { ExtendedFieldsData, FieldsData, RunWorkflowParamType } from "./state"; + +interface RunWorkflowHistoryTableProps { + workflowName?: string; + fillReRunWfFields: (data: RunWorkflowParamType) => void; + workflowHistory: ExtendedFieldsData[]; + setWorkflowHistory: (data: FieldsData[]) => void; +} + +export const RunWorkflowHistoryTable: FunctionComponent< + RunWorkflowHistoryTableProps +> = ({ + workflowName, + fillReRunWfFields, + workflowHistory, + setWorkflowHistory, +}) => { + const filteredWorkflowHistory = useMemo(() => { + let newHistory = []; + if (workflowName && Array.isArray(workflowHistory)) { + newHistory = workflowHistory.filter((item) => item.name === workflowName); + return newHistory; + } + return workflowHistory; + }, [workflowName, workflowHistory]); + const [showConfirmDialog, setShowConfirmDialog] = useState(false); + const showExecution = (executionlink: string) => { + if (Array.isArray(workflowHistory)) { + const found = workflowHistory.find( + (el) => el.executionLink === executionlink, + ); + if (!found) { + return; + } + window.open(`/execution/${executionlink}`, "_blank", "noreferrer"); + } + }; + + const handleClearWorkflowHistory = () => { + let remainingHistory: FieldsData[] = []; + if (_isEqual(workflowHistory, filteredWorkflowHistory)) { + setWorkflowHistory([]); + } else { + remainingHistory = _difference(workflowHistory, filteredWorkflowHistory); + setWorkflowHistory(remainingHistory); + } + }; + + return ( + + {showConfirmDialog && ( + { + if (confirmed) { + handleClearWorkflowHistory(); + setShowConfirmDialog(false); + } else { + setShowConfirmDialog(false); + } + }} + message={"Are you sure you want to delete the Run Workflow History?"} + /> + )} + + History is empty + + } + defaultSortFieldId="executionTime" + defaultSortAsc={false} + columns={[ + { + id: "name", + name: "name", + label: "Execution name", + grow: 0.5, + renderer: (val, row) => { + return ( +
    + {row.executionLink ? ( + showExecution(row.executionLink)} + target="noreferrer noopener" + > + {row.name} + + ) : ( + row.name + )} +
    + ); + }, + }, + { + id: "executionLink", + name: "executionLink", + label: "Execution link", + grow: 0, + center: true, + renderer: (val) => { + return ( +
    + {val && ( + showExecution(val)} + //target="noreferrer noopener" + // path={`/execution/${val}`} + > + + + )} +
    + ); + }, + }, + { + id: "executionTime", + name: "executionTime", + label: "Execution time", + grow: 0.25, + renderer: (val) => { + return new Date(val).toLocaleString(); + }, + }, + { + id: "useData", + name: "useData", + label: "Restore form values", + grow: 0.25, + right: true, + renderer: (val, row) => { + return ( + { + fillReRunWfFields(row); + }} + > + + + ); + }, + }, + ]} + data={filteredWorkflowHistory} + actions={[ + + + , + ]} + /> +
    + ); +}; diff --git a/ui-next/src/pages/definition/RunWorkflow/index.ts b/ui-next/src/pages/definition/RunWorkflow/index.ts new file mode 100644 index 0000000000..c419123621 --- /dev/null +++ b/ui-next/src/pages/definition/RunWorkflow/index.ts @@ -0,0 +1 @@ +export * from "./RunWorkflowForm"; diff --git a/ui-next/src/pages/definition/RunWorkflow/state/actions.ts b/ui-next/src/pages/definition/RunWorkflow/state/actions.ts new file mode 100644 index 0000000000..fd660bc06c --- /dev/null +++ b/ui-next/src/pages/definition/RunWorkflow/state/actions.ts @@ -0,0 +1,169 @@ +import { DoneInvokeEvent, assign, sendParent } from "xstate"; +import { + ClearFormEvent, + HandlePopoverMessageEvent, + RunMachineContext, + RunWorkflowParamType, + UpdateAllFieldsEvent, + UpdateCorrelationIdEvent, + UpdateIdempotencyKeyEvent, + UpdateInputParamsEvent, + UpdateTasksToDomainEvent, + UpdateIdempotencyStrategyEvent, + UpdateIdempotencyValuesEvent, +} from "./types"; +import { getTemplateFromInputParams } from "pages/runWorkflow/runWorkflowUtils"; +import { WorkflowDef } from "types/WorkflowDef"; +import { DefinitionMachineEventTypes } from "pages/definition/state"; +import { tryToJson } from "utils/utils"; +import _pick from "lodash/pick"; +import _isEmpty from "lodash/isEmpty"; +import { sendTo } from "xstate/lib/actions"; +import { ErrorInspectorEventTypes } from "pages/definition/errorInspector/state"; + +const templateFromInputParams = (currentWf: Partial) => + currentWf && + currentWf["inputParameters"] && + currentWf["inputParameters"].length > 0 + ? getTemplateFromInputParams(currentWf["inputParameters"]) + : "{}"; + +const shouldPreserveInput = (input?: string) => + input && input.trim() !== "" && input.trim() !== "{}"; + +const getInputFromHistory = (currentWf: any) => { + const history: RunWorkflowParamType[] = + tryToJson(localStorage.getItem("workflowHistory")) || []; + const filtered = history.filter((item) => item.name === currentWf.name); + if (filtered.length > 0) { + const historyInput = filtered[0].input ?? {}; + const wfInput = + tryToJson(getTemplateFromInputParams(currentWf["inputParameters"])) || {}; + let merged = { ...wfInput, ...historyInput }; + merged = _pick(merged, currentWf.inputParameters ?? []); + return JSON.stringify(merged, null, 2); + } + return null; +}; + +export const persistInputParams = assign< + RunMachineContext, + UpdateInputParamsEvent +>({ + input: (_context, { changes }) => changes, +}); + +export const persistCorrelationId = assign< + RunMachineContext, + UpdateCorrelationIdEvent +>({ + correlationId: (_context, { changes }) => changes, +}); + +export const persistIdempotencyKey = assign< + RunMachineContext, + UpdateIdempotencyKeyEvent +>({ + idempotencyKey: (_context, { changes }) => changes, +}); + +export const persistIdempotencyStrategy = assign< + RunMachineContext, + UpdateIdempotencyStrategyEvent +>({ + idempotencyStrategy: (_context, { changes }) => changes, +}); + +export const persistIdempotencyValues = assign< + RunMachineContext, + UpdateIdempotencyValuesEvent +>({ + idempotencyKey: (_context, { changes }) => changes?.idempotencyKey, + idempotencyStrategy: (_context, { changes }) => changes?.idempotencyStrategy, +}); + +export const persistTasksToDomain = assign< + RunMachineContext, + UpdateTasksToDomainEvent +>({ + taskToDomain: (_context, { changes }) => changes, +}); +export const clearForm = assign( + (context, _data) => { + return { + taskToDomain: "{}", + correlationId: "", + input: templateFromInputParams(context.currentWf ?? {}), + idempotencyKey: "", + }; + }, +); + +export const checkForExistingInputParams = assign< + RunMachineContext, + DoneInvokeEvent +>((context) => { + if (shouldPreserveInput(context.input)) { + return {}; + } + + let input = getInputFromHistory(context.currentWf); + + // If no history, check for default parameters first + if ( + (!input || input.trim() === "{}") && + !_isEmpty(context.workflowDefaultRunParam) + ) { + input = JSON.stringify(context.workflowDefaultRunParam, null, 2); + } + // Only fall back to empty template if no history and no defaults + if (!input) { + input = templateFromInputParams(context.currentWf ?? {}); + } + + return { + input, + }; +}); + +export const persistPopupMessage = assign< + RunMachineContext, + HandlePopoverMessageEvent +>((_, { popoverMessage }) => ({ + popoverMessage, +})); + +export const redirectToNewExecution = sendParent( + (context: RunMachineContext, { data }: { type: string; data: string }) => ({ + type: DefinitionMachineEventTypes.REDIRECT_TO_EXECUTION_PAGE, + executionId: data, + }), +); + +export const persistAllFields = assign( + (_context, { data }) => data, +); + +export const reportErrorToErrorInspector = sendTo( + ({ errorInspectorMachine }) => errorInspectorMachine, + (_context, { data }: DoneInvokeEvent<{ message: string }>) => ({ + type: ErrorInspectorEventTypes.REPORT_RUN_ERROR, + text: data.message, + }), +); + +export const sendContextToParent = sendParent( + (context: RunMachineContext, event) => ({ + type: DefinitionMachineEventTypes.SYNC_RUN_CONTEXT_AND_CHANGE_TAB, + data: { + originalEvent: event, + runMachineContext: { + input: context.input, + correlationId: context.correlationId, + taskToDomain: context.taskToDomain, + idempotencyKey: context.idempotencyKey, + idempotencyStrategy: context.idempotencyStrategy, + }, + }, + }), +); diff --git a/ui-next/src/pages/definition/RunWorkflow/state/hook.ts b/ui-next/src/pages/definition/RunWorkflow/state/hook.ts new file mode 100644 index 0000000000..8f233cce35 --- /dev/null +++ b/ui-next/src/pages/definition/RunWorkflow/state/hook.ts @@ -0,0 +1,136 @@ +import { ActorRef, State } from "xstate"; +import { useSelector } from "@xstate/react"; +import { + RunMachineContext, + RunMachineEvents, + RunMachineEventsTypes, + RunMachineStates, + FieldsData, + IdempotencyStrategyEnum, + IdempotencyValuesProp, +} from "./types"; +import { PopoverMessage } from "types/Messages"; + +export const useRunTabActor = (actor: ActorRef) => { + const currentWf = useSelector( + actor, + (state: State) => state.context.currentWf, + ); + const input = useSelector( + actor, + (state: State) => state.context.input, + ); + const correlationId = useSelector( + actor, + (state: State) => state.context.correlationId, + ); + const idempotencyKey = useSelector( + actor, + (state: State) => state.context.idempotencyKey, + ); + const idempotencyStrategy = useSelector( + actor, + (state: State) => state.context.idempotencyStrategy, + ); + + const taskToDomain = useSelector( + actor, + (state: State) => state.context.taskToDomain, + ); + + const isRunning = useSelector(actor, (state) => + state.matches(RunMachineStates.RUN_WORKFLOW), + ); + const popoverMessage = useSelector( + actor, + (state: State) => state.context.popoverMessage, + ); + const handleChangeInputParams = (changes: string) => { + actor.send({ + type: RunMachineEventsTypes.UPDATE_INPUT_PARAMS, + changes, + }); + }; + const handleChangeCorrelationId = (changes: string) => { + actor.send({ + type: RunMachineEventsTypes.UPDATE_CORRELATION_ID, + changes, + }); + }; + const handleChangeIdempotencyKey = (changes: string) => { + actor.send({ + type: RunMachineEventsTypes.UPDATE_IDEMPOTENCY_KEY, + changes, + }); + }; + const handleChangeIdempotencyStrategy = ( + changes: IdempotencyStrategyEnum, + ) => { + actor.send({ + type: RunMachineEventsTypes.UPDATE_IDEMPOTENCY_STRATEGY, + changes, + }); + }; + + const handleChangeIdempotencyValues = (changes: IdempotencyValuesProp) => { + actor.send({ + type: RunMachineEventsTypes.UPDATE_IDEMPOTENCY_VALUES, + changes, + }); + }; + const handleChangeTasksToDomain = (changes: string) => { + actor.send({ + type: RunMachineEventsTypes.UPDATE_TASKS_TO_DOMAIN_MAPPING, + changes, + }); + }; + const handleClearForm = () => { + actor.send({ + type: RunMachineEventsTypes.CLEAR_FORM, + }); + }; + const handleRunThisWorkflow = () => { + actor.send({ + type: RunMachineEventsTypes.TRIGGER_RUN_WORKFLOW, + }); + }; + + const handlePopoverMessage = (popoverMessage: PopoverMessage | null) => { + actor.send({ + type: RunMachineEventsTypes.HANDLE_POPOVER_MESSAGE, + popoverMessage, + }); + }; + + const handleFillAllFields = (data: FieldsData) => { + actor.send({ + type: RunMachineEventsTypes.UPDATE_ALL_FIELDS, + data, + }); + }; + + return [ + { + currentWf, + input, + correlationId, + taskToDomain, + isRunning, + popoverMessage, + idempotencyKey, + idempotencyStrategy, + }, + { + handleChangeInputParams, + handleChangeCorrelationId, + handleChangeTasksToDomain, + handleClearForm, + handleRunThisWorkflow, + handlePopoverMessage, + handleFillAllFields, + handleChangeIdempotencyKey, + handleChangeIdempotencyStrategy, + handleChangeIdempotencyValues, + }, + ] as const; +}; diff --git a/ui-next/src/pages/definition/RunWorkflow/state/index.ts b/ui-next/src/pages/definition/RunWorkflow/state/index.ts new file mode 100644 index 0000000000..5ad9f97cc0 --- /dev/null +++ b/ui-next/src/pages/definition/RunWorkflow/state/index.ts @@ -0,0 +1,5 @@ +export * from "./machine"; +export * from "./types"; +export * from "./actions"; +export * from "./hook"; +export * from "./services"; diff --git a/ui-next/src/pages/definition/RunWorkflow/state/machine.ts b/ui-next/src/pages/definition/RunWorkflow/state/machine.ts new file mode 100644 index 0000000000..6a6d37cb12 --- /dev/null +++ b/ui-next/src/pages/definition/RunWorkflow/state/machine.ts @@ -0,0 +1,89 @@ +import { createMachine } from "xstate"; +import * as actions from "./actions"; +import * as services from "./services"; +import { + RunMachineContext, + RunMachineEventsTypes, + RunMachineEvents, + RunMachineStates, +} from "./types"; + +export const runMachine = createMachine( + { + predictableActionArguments: true, + id: "runWorkflowMachine", + initial: RunMachineStates.CHECK_INPUT_PARAMS, + context: { + authHeaders: undefined, + currentWf: {}, + input: "{}", + workflowDefaultRunParam: undefined, + correlationId: undefined, + errorInspectorMachine: undefined, + taskToDomain: "{}", + popoverMessage: null, + idempotencyKey: undefined, + idempotencyStrategy: undefined, + }, + states: { + [RunMachineStates.IDLE]: { + on: { + [RunMachineEventsTypes.UPDATE_INPUT_PARAMS]: { + actions: ["persistInputParams"], + }, + [RunMachineEventsTypes.UPDATE_CORRELATION_ID]: { + actions: ["persistCorrelationId"], + }, + [RunMachineEventsTypes.UPDATE_IDEMPOTENCY_KEY]: { + actions: ["persistIdempotencyKey"], + }, + [RunMachineEventsTypes.UPDATE_IDEMPOTENCY_VALUES]: { + actions: ["persistIdempotencyValues"], + }, + [RunMachineEventsTypes.UPDATE_IDEMPOTENCY_STRATEGY]: { + actions: ["persistIdempotencyStrategy"], + }, + [RunMachineEventsTypes.UPDATE_TASKS_TO_DOMAIN_MAPPING]: { + actions: ["persistTasksToDomain"], + }, + [RunMachineEventsTypes.CLEAR_FORM]: { + actions: ["clearForm"], + }, + [RunMachineEventsTypes.TRIGGER_RUN_WORKFLOW]: { + target: RunMachineStates.RUN_WORKFLOW, + }, + [RunMachineEventsTypes.HANDLE_POPOVER_MESSAGE]: { + actions: ["persistPopupMessage"], + }, + [RunMachineEventsTypes.UPDATE_ALL_FIELDS]: { + actions: ["persistAllFields"], + }, + [RunMachineEventsTypes.CHANGE_TAB_EVT]: { + actions: ["sendContextToParent"], + }, + }, + }, + [RunMachineStates.CHECK_INPUT_PARAMS]: { + entry: ["checkForExistingInputParams"], + always: RunMachineStates.IDLE, + }, + [RunMachineStates.RUN_WORKFLOW]: { + invoke: { + src: "runWorkflow", + onDone: { + actions: ["redirectToNewExecution"], + target: RunMachineStates.IDLE, + }, + onError: { + actions: ["reportErrorToErrorInspector"], + target: RunMachineStates.IDLE, + }, + }, + }, + }, + }, + { + services: services as any, + actions: actions as any, + }, +); diff --git a/ui-next/src/pages/definition/RunWorkflow/state/services.ts b/ui-next/src/pages/definition/RunWorkflow/state/services.ts new file mode 100644 index 0000000000..13c987adf3 --- /dev/null +++ b/ui-next/src/pages/definition/RunWorkflow/state/services.ts @@ -0,0 +1,77 @@ +import { tryToJson, tryFunc } from "utils/utils"; +import { RunMachineContext } from "./types"; +import { fetchWithContext } from "plugins/fetch"; +import { v4 as uuidv4 } from "uuid"; +const GENERIC_ERROR_MESSAGE = "Error while running workflow."; + +export const runWorkflow = async ( + { + authHeaders, + input: inputParams, + taskToDomain: tasksToDomain, + correlationId, + currentWf, + idempotencyKey, + idempotencyStrategy, + }: RunMachineContext, + __: any, +) => { + return tryFunc({ + fn: async () => { + const RECORD_LIMIT = 20; + const input = tryToJson(inputParams); + const taskToDomain = tryToJson(tasksToDomain); + const postObject = { + name: currentWf?.name, + version: currentWf?.version, + correlationId, + input, + taskToDomain, + idempotencyKey, + ...(idempotencyKey && + idempotencyStrategy && { + idempotencyStrategy: idempotencyStrategy, + }), + }; + const postBody = JSON.stringify(postObject); + const result = await fetchWithContext( + "/workflow", + {}, + { + method: "POST", + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + body: postBody, + }, + true, + ); + + // store history in local storage + const existingHistory: [] = + tryToJson(localStorage.getItem("workflowHistory")) || []; + const newHistoryItem = { + id: uuidv4(), + executionLink: result, + executionTime: Date.now(), + }; + + if (postObject) { + Object.assign(newHistoryItem, postObject); + } + localStorage.setItem( + "workflowHistory", + JSON.stringify( + [newHistoryItem, ...existingHistory].slice(0, RECORD_LIMIT), + ), + ); + + return result; + }, + customError: { + message: GENERIC_ERROR_MESSAGE, + }, + showCustomError: false, + }); +}; diff --git a/ui-next/src/pages/definition/RunWorkflow/state/types.ts b/ui-next/src/pages/definition/RunWorkflow/state/types.ts new file mode 100644 index 0000000000..2de8c99b17 --- /dev/null +++ b/ui-next/src/pages/definition/RunWorkflow/state/types.ts @@ -0,0 +1,136 @@ +import { ActorRef } from "xstate"; +import { PopoverMessage, WorkflowDef } from "types"; +import { AuthHeaders } from "types"; +import { ErrorInspectorMachineEvents } from "pages/definition/errorInspector/state/types"; + +export enum IdempotencyStrategyEnum { + FAIL = "FAIL", + RETURN_EXISTING = "RETURN_EXISTING", + FAIL_ON_RUNNING = "FAIL_ON_RUNNING", +} + +export type IdempotencyValuesProp = { + idempotencyKey: string; + idempotencyStrategy?: IdempotencyStrategyEnum; +}; + +type CommonProperties = { + correlationId: string; + input: object; + taskToDomain?: object; + idempotencyKey?: string; + idempotencyStrategy?: IdempotencyStrategyEnum; +}; + +export type RunWorkflowParamType = { + name: string; + version: string; +} & CommonProperties; + +export type FieldsData = { + input: string; + taskToDomain: string; + correlationId: string; + idempotencyKey?: string; + idempotencyStrategy?: IdempotencyStrategyEnum; +}; + +export type ExtendedFieldsData = FieldsData & { + name: string; + executionLink: string; +}; + +export interface RunMachineContext { + authHeaders?: AuthHeaders; + currentWf: Partial; + input?: string; + correlationId?: string; + taskToDomain?: string; + popoverMessage: PopoverMessage | null; + errorInspectorMachine?: ActorRef; + idempotencyKey?: string; + idempotencyStrategy?: IdempotencyStrategyEnum; + workflowDefaultRunParam?: Record; +} + +export enum RunMachineStates { + IDLE = "IDLE", + CHECK_INPUT_PARAMS = "CHECK_INPUT_PARAMS", + RUN_WORKFLOW = "RUN_WORKFLOW", +} + +export enum RunMachineEventsTypes { + UPDATE_INPUT_PARAMS = "UPDATE_INPUT_PARAMS", + UPDATE_CORRELATION_ID = "UPDATE_CORRELATION_ID", + UPDATE_TASKS_TO_DOMAIN_MAPPING = "UPDATE_TASKS_TO_DOMAIN_MAPPING", + CLEAR_FORM = "CLEAR_FORM", + TRIGGER_RUN_WORKFLOW = "TRIGGER_RUN_WORKFLOW", + HANDLE_POPOVER_MESSAGE = "HANDLE_POPOVER_MESSAGE", + UPDATE_ALL_FIELDS = "UPDATE_ALL_FIELDS", + UPDATE_IDEMPOTENCY_STRATEGY = "UPDATE_IDEMPOTENCY_STRATEGY", + UPDATE_IDEMPOTENCY_KEY = "UPDATE_IDEMPOTENCY_KEY", + UPDATE_IDEMPOTENCY_VALUES = "UPDATE_IDEMPOTENCY_VALUES", + CHANGE_TAB_EVT = "changeTab", +} + +export type UpdateInputParamsEvent = { + type: RunMachineEventsTypes.UPDATE_INPUT_PARAMS; + changes: string; +}; +export type UpdateCorrelationIdEvent = { + type: RunMachineEventsTypes.UPDATE_CORRELATION_ID; + changes: string; +}; +export type UpdateIdempotencyKeyEvent = { + type: RunMachineEventsTypes.UPDATE_IDEMPOTENCY_KEY; + changes: string; +}; +export type UpdateIdempotencyStrategyEvent = { + type: RunMachineEventsTypes.UPDATE_IDEMPOTENCY_STRATEGY; + changes: IdempotencyStrategyEnum; +}; + +export type UpdateIdempotencyValuesEvent = { + type: RunMachineEventsTypes.UPDATE_IDEMPOTENCY_VALUES; + changes: IdempotencyValuesProp; +}; + +export type UpdateTasksToDomainEvent = { + type: RunMachineEventsTypes.UPDATE_TASKS_TO_DOMAIN_MAPPING; + changes: string; +}; + +export type ClearFormEvent = { + type: RunMachineEventsTypes.CLEAR_FORM; +}; + +export type RunWorkflowEvent = { + type: RunMachineEventsTypes.TRIGGER_RUN_WORKFLOW; +}; + +export type HandlePopoverMessageEvent = { + type: RunMachineEventsTypes.HANDLE_POPOVER_MESSAGE; + popoverMessage: PopoverMessage | null; +}; + +export type UpdateAllFieldsEvent = { + type: RunMachineEventsTypes.UPDATE_ALL_FIELDS; + data: FieldsData; +}; + +export type ChangeTabEvent = { + type: RunMachineEventsTypes.CHANGE_TAB_EVT; +}; + +export type RunMachineEvents = + | UpdateInputParamsEvent + | UpdateCorrelationIdEvent + | UpdateTasksToDomainEvent + | ClearFormEvent + | RunWorkflowEvent + | HandlePopoverMessageEvent + | UpdateAllFieldsEvent + | UpdateIdempotencyStrategyEvent + | UpdateIdempotencyKeyEvent + | UpdateIdempotencyValuesEvent + | ChangeTabEvent; diff --git a/ui-next/src/pages/definition/WorkflowDefinition.tsx b/ui-next/src/pages/definition/WorkflowDefinition.tsx new file mode 100644 index 0000000000..9575ba604b --- /dev/null +++ b/ui-next/src/pages/definition/WorkflowDefinition.tsx @@ -0,0 +1,123 @@ +import { AlertColor, Box } from "@mui/material"; +import { useSelector } from "@xstate/react"; +import { SnackbarMessage } from "components/SnackbarMessage"; +import TwoPanesDivider from "components/TwoPanesDivider"; +import { + DefinitionMachineContext, + FlowEditContextProvider, + WorkflowDefinitionEvents, +} from "pages/definition/state"; +import { Helmet } from "react-helmet"; +import { useAuth } from "shared/auth"; +import { ActorRef, State } from "xstate"; +import sharedStyles from "../styles"; +import EditorPanel from "./EditorPanel/EditorPanel"; +import GraphPanel from "./GraphPanel"; +import { PromptIfChanges } from "./PromptIfChanges"; +import { useWorkflowDefinition } from "./state/hook"; +import { WorkflowMetaBar } from "./WorkflowMetadata"; + +export default function Workflow() { + const { conductorUser } = useAuth(); + const [ + { handleResetMessage, setLeftPanelExpanded }, + { workflowName, message, definitionActor, leftPanelExpanded }, + ] = useWorkflowDefinition(conductorUser!); + + const graphPanel = ; + + const editorPanel = definitionActor && ( + + ); + + const isReady = useSelector( + definitionActor, + (state: State) => state.matches("ready"), + ); + + return ( + <> + {isReady && definitionActor && ( + + )} + + + Workflow Definition - {workflowName || "NEW"} + + + + + + + {/* {showImportSuccessfulDialog && fetchingWfSuccessful && ( + + )} */} + + } + /> + + + {definitionActor?.children.get("flowMachine") && ( + + )} + + + + + + + ); +} diff --git a/ui-next/src/pages/definition/WorkflowMetadata/EditInPlaceWrapper/EditInPlaceFieldWrapper.tsx b/ui-next/src/pages/definition/WorkflowMetadata/EditInPlaceWrapper/EditInPlaceFieldWrapper.tsx new file mode 100644 index 0000000000..17da6b61bf --- /dev/null +++ b/ui-next/src/pages/definition/WorkflowMetadata/EditInPlaceWrapper/EditInPlaceFieldWrapper.tsx @@ -0,0 +1,67 @@ +import { ActorRef } from "xstate"; +import { CSSProperties, useRef } from "react"; +import { useActor, useSelector } from "@xstate/react"; +import EditInPlace, { EditInPlaceProps } from "components/EditInPlace"; +import { InputBase } from "@mui/material"; +import { EditInPlaceEventTypes, EditInPlaceMachineEvents } from "./state"; +import { FunctionComponent } from "react"; + +interface EditorInPlaceFieldWrapperProps extends Omit< + EditInPlaceProps, + "isEditing" | "setEditing" | "text" | "childRef" +> { + editInPlaceActor: ActorRef; + inputStyles?: CSSProperties; +} + +export const EditInPlaceFieldWrapper: FunctionComponent< + EditorInPlaceFieldWrapperProps +> = ({ + editInPlaceActor, + disabled, + inputStyles, + style, + ...editInPlaceProps +}) => { + const [, send] = useActor(editInPlaceActor); + const isEditing = useSelector(editInPlaceActor, (state) => + state.matches("editing"), + ); + const fieldValue = useSelector( + editInPlaceActor, + (state) => state.context.value, + ); + const handleChange = (value: string) => { + send({ type: EditInPlaceEventTypes.CHANGE_VALUE, value }); + }; + + const handleToggleEditing = () => { + send({ type: EditInPlaceEventTypes.TOGGLE_EDITING }); + }; + + const inputRef = useRef(null); + return ( + + { + handleChange(value); + }} + style={inputStyles} + id="edit-inline-input-base" + /> + + ); +}; diff --git a/ui-next/src/pages/definition/WorkflowMetadata/EditInPlaceWrapper/state/actions.ts b/ui-next/src/pages/definition/WorkflowMetadata/EditInPlaceWrapper/state/actions.ts new file mode 100644 index 0000000000..83eea35625 --- /dev/null +++ b/ui-next/src/pages/definition/WorkflowMetadata/EditInPlaceWrapper/state/actions.ts @@ -0,0 +1,21 @@ +import { assign, sendParent } from "xstate"; +import { cancel } from "xstate/lib/actions"; +import { EditInPlaceMachineContext, ChangeValueEvent } from "./types"; +import { WorkflowMetadataMachineEventTypes } from "../../state/types"; + +export const persistChanges = assign< + EditInPlaceMachineContext, + ChangeValueEvent +>({ + value: (_context, { value }) => value, +}); + +export const debounceSyncWithParent = sendParent( + (context: EditInPlaceMachineContext, { value }: ChangeValueEvent) => ({ + type: WorkflowMetadataMachineEventTypes.UPDATE_METADATA, + metadataChanges: { [context.fieldName]: value }, + }), + /* { delay: 500, id: "sync_with_parent" } // Use function to reduce debounce if code tab is opened. */ +); + +export const cancelSyncWithParent = cancel("sync_with_parent"); diff --git a/ui-next/src/pages/definition/WorkflowMetadata/EditInPlaceWrapper/state/guards.ts b/ui-next/src/pages/definition/WorkflowMetadata/EditInPlaceWrapper/state/guards.ts new file mode 100644 index 0000000000..0d8034876b --- /dev/null +++ b/ui-next/src/pages/definition/WorkflowMetadata/EditInPlaceWrapper/state/guards.ts @@ -0,0 +1,12 @@ +import { EditInPlaceMachineContext, ChangeValueEvent } from "./types"; + +export const hasValidChars = ( + context: EditInPlaceMachineContext, + { value }: ChangeValueEvent, +) => { + if (context.allowedCharsRegEx) { + const regEx = new RegExp(context.allowedCharsRegEx); + return regEx.test(value); + } + return true; +}; diff --git a/ui-next/src/pages/definition/WorkflowMetadata/EditInPlaceWrapper/state/index.ts b/ui-next/src/pages/definition/WorkflowMetadata/EditInPlaceWrapper/state/index.ts new file mode 100644 index 0000000000..53401e46b9 --- /dev/null +++ b/ui-next/src/pages/definition/WorkflowMetadata/EditInPlaceWrapper/state/index.ts @@ -0,0 +1,2 @@ +export * from "./types"; +export * from "./machine"; diff --git a/ui-next/src/pages/definition/WorkflowMetadata/EditInPlaceWrapper/state/machine.ts b/ui-next/src/pages/definition/WorkflowMetadata/EditInPlaceWrapper/state/machine.ts new file mode 100644 index 0000000000..4122478785 --- /dev/null +++ b/ui-next/src/pages/definition/WorkflowMetadata/EditInPlaceWrapper/state/machine.ts @@ -0,0 +1,55 @@ +import { createMachine } from "xstate"; +import * as actions from "./actions"; +import { + EditInPlaceMachineContext, + EditInPlaceEventTypes, + EditInPlaceMachineEvents, +} from "./types"; +import * as guards from "./guards"; + +export const editInPlaceMachine = createMachine< + EditInPlaceMachineContext, + EditInPlaceMachineEvents +>( + { + id: "editInPlaceMachine", + initial: "notEditing", + predictableActionArguments: true, + context: { + value: "", + fieldName: "", + }, + on: { + [EditInPlaceEventTypes.DISABLE_EDITING]: { + target: "notEditing", + }, + }, + states: { + editing: { + on: { + [EditInPlaceEventTypes.CHANGE_VALUE]: { + cond: "hasValidChars", + actions: ["persistChanges", "debounceSyncWithParent"], + }, + [EditInPlaceEventTypes.TOGGLE_EDITING]: { + target: "notEditing", + }, + }, + }, + notEditing: { + on: { + [EditInPlaceEventTypes.TOGGLE_EDITING]: { + target: "editing", + }, + [EditInPlaceEventTypes.VALUE_UPDATED]: { + actions: ["persistChanges"], + }, + }, + }, + }, + }, + { + actions: actions as any, + guards: guards as any, + }, +); diff --git a/ui-next/src/pages/definition/WorkflowMetadata/EditInPlaceWrapper/state/types.ts b/ui-next/src/pages/definition/WorkflowMetadata/EditInPlaceWrapper/state/types.ts new file mode 100644 index 0000000000..3a1c83f67b --- /dev/null +++ b/ui-next/src/pages/definition/WorkflowMetadata/EditInPlaceWrapper/state/types.ts @@ -0,0 +1,36 @@ +export interface EditInPlaceMachineContext { + value: string; + fieldName: string; + allowedCharsRegEx?: string; +} + +export enum EditInPlaceEventTypes { + TOGGLE_EDITING = "TOGGLE_EDITING", + CHANGE_VALUE = "CHANGE_VALUE", + VALUE_UPDATED = "VALUE_UPDATED", + DISABLE_EDITING = "DISABLE_EDITING", +} + +export type ToggleEditingEvent = { + type: EditInPlaceEventTypes.TOGGLE_EDITING; +}; + +export type ChangeValueEvent = { + type: EditInPlaceEventTypes.CHANGE_VALUE; + value: string; +}; + +export type ValueUpdatedEvent = { + type: EditInPlaceEventTypes.VALUE_UPDATED; + value: string; +}; + +export type DisableEditingEvent = { + type: EditInPlaceEventTypes.DISABLE_EDITING; +}; + +export type EditInPlaceMachineEvents = + | ToggleEditingEvent + | ChangeValueEvent + | DisableEditingEvent + | ValueUpdatedEvent; diff --git a/ui-next/src/pages/definition/WorkflowMetadata/WorkflowMetaBar.tsx b/ui-next/src/pages/definition/WorkflowMetadata/WorkflowMetaBar.tsx new file mode 100644 index 0000000000..278ace39c3 --- /dev/null +++ b/ui-next/src/pages/definition/WorkflowMetadata/WorkflowMetaBar.tsx @@ -0,0 +1,212 @@ +import { Box, Grid } from "@mui/material"; +import { useSelector } from "@xstate/react"; +import { Text } from "components"; +import { selectIsOpenedEdge } from "components/flow/state/selectors"; +import { HeadBarSelect } from "components/v1"; +import ConductorBreadcrumbs from "components/v1/ConductorBreadcrumbs"; +import DoubleArrowLeftIcon from "components/v1/icons/DoubleArrowLeftIcon"; +import ButtonLinks from "components/v1/layout/header/ButtonLinks"; +import _isString from "lodash/isString"; +import _isUndefined from "lodash/isUndefined"; +import _uniq from "lodash/uniq"; +import { FunctionComponent, useMemo } from "react"; +import { useContainerQuery } from "react-container-query"; +import { colors } from "theme/tokens/variables"; +import { ActorRef } from "xstate"; +import { HeadActionButtons } from "../EditorPanel/HeadActionButtons"; +import { + isSaveRequestSelector, + versionSelector, + versionsSelector, +} from "../EditorPanel/selectors"; +import { ConfirmSaveButtonGroup } from "../confirmSave"; +import { + DefinitionMachineEventTypes, + WorkflowDefinitionEvents, +} from "../state"; + +const metaBarQuery = { + small: { + maxWidth: 699, + }, + large: { + minWidth: 700, + }, +}; +/* THIS IS NOT THE METABAR THIS IS ACTUALLY THE HEADER. NOT RENAMING SINCE WE ARE MOVING TO VITE. WE WILL CONFUSE IT */ +interface WorkflowMetaBarProps { + leftPanelExpanded: boolean; // We should get rid of this move it to xstate + setLeftPanelExpanded: (t: boolean) => void; + definitionActor: ActorRef; +} + +export const WorkflowMetaBar: FunctionComponent = ({ + leftPanelExpanded, + setLeftPanelExpanded, + definitionActor, +}) => { + const version = useSelector(definitionActor, versionSelector); + const versions = useSelector(definitionActor, versionsSelector); + const isSaveRequest = useSelector(definitionActor, isSaveRequestSelector); + const flowActor = (definitionActor as any)?.children?.get("flowMachine"); + const isNewWorkflow = useSelector( + definitionActor, + (state) => state.context?.isNewWorkflow, + ); + + const openedEdge = useSelector(flowActor, selectIsOpenedEdge); + + const handleChangeVersion = (version: string) => + definitionActor.send({ + type: DefinitionMachineEventTypes.CHANGE_VERSION_EVT, + version, + }); + + const name = useSelector( + definitionActor, + (state) => state.context?.workflowChanges?.name, + ); + const [_containerQueryState, containerRef] = useContainerQuery(metaBarQuery, { + width: 600, + height: 800, + }); + + const saveChangesActor = (definitionActor as any).children?.get( + "saveChangesMachine", + ); + + const maybeConfirmSaveButtonGroup = useMemo( + () => + isSaveRequest && saveChangesActor ? ( + + ) : null, + [saveChangesActor, isSaveRequest], + ); + + const breadcrumbItems = [ + { label: "Workflow Definitions", to: "/workflowDef" }, + { label: name, to: "" }, + ]; + + return ( + <> + + theme.palette?.mode === "dark" ? colors.gray14 : undefined, + backgroundColor: (theme) => + theme.palette?.mode === "dark" ? colors.gray00 : colors.gray14, + position: "relative", + }} + > + + + + + {_isString(name) ? name : ""} + + + + + + handleChangeVersion(e)} + items={[ + ...(_uniq(versions || [])?.sort((a, b) => a - b) || []), + ...(!isNewWorkflow + ? [{ label: "Latest version", value: "" }] + : []), + ].map((ver) => + typeof ver === "number" + ? { label: `Version ${ver}`, value: ver.toString() } + : ver, + )} + labelOnEmpty="Latest version" + /> + {maybeConfirmSaveButtonGroup} + {!(definitionActor as any).children?.get( + "saveChangesMachine", + ) && } + + + + {leftPanelExpanded && !openedEdge && ( + + { + setLeftPanelExpanded(false); + }} + sx={{ + fontSize: "12px", + fontWeight: 400, + display: "flex", + alignItems: "center", + }} + > + Open panel + + + )} + + + ); +}; diff --git a/ui-next/src/pages/definition/WorkflowMetadata/index.ts b/ui-next/src/pages/definition/WorkflowMetadata/index.ts new file mode 100644 index 0000000000..bdd9142cc6 --- /dev/null +++ b/ui-next/src/pages/definition/WorkflowMetadata/index.ts @@ -0,0 +1 @@ +export * from "./WorkflowMetaBar"; diff --git a/ui-next/src/pages/definition/WorkflowMetadata/state/WorkflowMetadataContext/WorkflowMetadataContext.tsx b/ui-next/src/pages/definition/WorkflowMetadata/state/WorkflowMetadataContext/WorkflowMetadataContext.tsx new file mode 100644 index 0000000000..9e593064d9 --- /dev/null +++ b/ui-next/src/pages/definition/WorkflowMetadata/state/WorkflowMetadataContext/WorkflowMetadataContext.tsx @@ -0,0 +1,7 @@ +import { createContext } from "react"; +import { WorkflowMetadataContextProviderProps } from "./types"; + +export const WorkflowMetadataContext = + createContext({ + workflowMetadataActor: undefined, + }); diff --git a/ui-next/src/pages/definition/WorkflowMetadata/state/WorkflowMetadataContext/WorkflowMetadataProvider.tsx b/ui-next/src/pages/definition/WorkflowMetadata/state/WorkflowMetadataContext/WorkflowMetadataProvider.tsx new file mode 100644 index 0000000000..075a10b14b --- /dev/null +++ b/ui-next/src/pages/definition/WorkflowMetadata/state/WorkflowMetadataContext/WorkflowMetadataProvider.tsx @@ -0,0 +1,11 @@ +import { FunctionComponent } from "react"; +import { WorkflowMetadataContext } from "./WorkflowMetadataContext"; +import { WorkflowMetadataContextProviderProps } from "./types"; + +export const WorkflowMetadataProvider: FunctionComponent< + WorkflowMetadataContextProviderProps +> = ({ children, workflowMetadataActor }) => ( + + {children} + +); diff --git a/ui-next/src/pages/definition/WorkflowMetadata/state/WorkflowMetadataContext/index.ts b/ui-next/src/pages/definition/WorkflowMetadata/state/WorkflowMetadataContext/index.ts new file mode 100644 index 0000000000..8a6d368d3e --- /dev/null +++ b/ui-next/src/pages/definition/WorkflowMetadata/state/WorkflowMetadataContext/index.ts @@ -0,0 +1,2 @@ +export * from "./WorkflowMetadataContext"; +export * from "./WorkflowMetadataProvider"; diff --git a/ui-next/src/pages/definition/WorkflowMetadata/state/WorkflowMetadataContext/types.ts b/ui-next/src/pages/definition/WorkflowMetadata/state/WorkflowMetadataContext/types.ts new file mode 100644 index 0000000000..78e5315838 --- /dev/null +++ b/ui-next/src/pages/definition/WorkflowMetadata/state/WorkflowMetadataContext/types.ts @@ -0,0 +1,8 @@ +import { ReactNode } from "react"; +import { ActorRef } from "xstate"; +import { WorkflowMetadataEvents } from "../types"; + +export interface WorkflowMetadataContextProviderProps { + workflowMetadataActor?: ActorRef; + children?: ReactNode; +} diff --git a/ui-next/src/pages/definition/WorkflowMetadata/state/actions.ts b/ui-next/src/pages/definition/WorkflowMetadata/state/actions.ts new file mode 100644 index 0000000000..4622b49a14 --- /dev/null +++ b/ui-next/src/pages/definition/WorkflowMetadata/state/actions.ts @@ -0,0 +1,96 @@ +import { assign, sendParent, spawn, forwardTo, DoneInvokeEvent } from "xstate"; +import { cancel, send, pure } from "xstate/lib/actions"; +import { + UpdateMetaDataEvent, + WorkflowChangedEvent, + WorkflowMetadataMachineContext, +} from "./types"; +import _get from "lodash/get"; +import { DefinitionMachineEventTypes } from "pages/definition/state/types"; +import { WorkflowDef } from "types/WorkflowDef"; +import { extractWorkflowMetadata } from "../../helpers"; +import { EditInPlaceEventTypes } from "../EditInPlaceWrapper/state/types"; +import { editInPlaceMachine } from "../EditInPlaceWrapper/state/machine"; +import { metadataFieldMachine } from "pages/definition/EditorPanel/WorkflowPropertiesFormTab/state/machine"; + +export const persistPartialMetaDataChanges = assign< + WorkflowMetadataMachineContext, + UpdateMetaDataEvent +>({ + metadataChanges: ( + { metadataChanges }, + { metadataChanges: partialChanges }, + ) => ({ ...metadataChanges, ...partialChanges }), +}); + +export const syncWithParent = sendParent( + (__, { metadataChanges }: UpdateMetaDataEvent) => ({ + type: DefinitionMachineEventTypes.UPDATE_WF_METADATA_EVT, + workflowMetadata: metadataChanges, + }), +); + +export const cancelSyncWithParent = cancel("sync_with_parent"); + +export const updateLocalCopy = assign< + WorkflowMetadataMachineContext, + WorkflowChangedEvent +>((context, { workflow }) => { + const metadata = extractWorkflowMetadata(workflow as Partial); + + return { + metadataChanges: metadata, + }; +}); + +// takes the machine name from context and spawns actors for each field +export const spawnFieldActors = assign({ + editableFieldActors: (context) => { + const childMachines = { + editInPlaceMachine: editInPlaceMachine, + metadataFieldMachine: metadataFieldMachine, + }; + const machineInstance = _get(childMachines, context.childActorsMachineName); + return context.editableFields.map((field) => + spawn( + machineInstance.withContext({ + value: _get(context.metadataChanges, field), + fieldName: field, + }), + `${field}-field`, + ), + ); + }, +}); + +// @ts-ignore +export const notifyActors = pure((context: WorkflowMetadataMachineContext) => { + return context.editableFields.map((field) => + send( + { + type: EditInPlaceEventTypes.VALUE_UPDATED, + value: _get(context.metadataChanges, field), + }, + { to: `${field}-field` }, + ), + ); +}); + +export const forwardActionToActors = pure( + // @ts-ignore + (context: WorkflowMetadataMachineContext) => { + return context.editableFields.map((field) => forwardTo(`${field}-field`)); + }, +); + +export const persistApplicationKeys = assign< + WorkflowMetadataMachineContext, + DoneInvokeEvent<{ id: string; secret: string }> +>((_context, { data }) => { + return { + applicationAccessKey: { + id: data.id, + secret: data.secret, + }, + }; +}); diff --git a/ui-next/src/pages/definition/WorkflowMetadata/state/guards.ts b/ui-next/src/pages/definition/WorkflowMetadata/state/guards.ts new file mode 100644 index 0000000000..608ca35ad9 --- /dev/null +++ b/ui-next/src/pages/definition/WorkflowMetadata/state/guards.ts @@ -0,0 +1,12 @@ +import { WorkflowMetadataMachineContext, WorkflowChangedEvent } from "./types"; +import { extractWorkflowMetadata } from "../../helpers"; +import fastDeepEqual from "fast-deep-equal"; + +export const hasMetadataChanges = ( + { metadataChanges }: WorkflowMetadataMachineContext, + { workflow }: WorkflowChangedEvent, +) => { + const sliceOfInterest = extractWorkflowMetadata(workflow); + const hasChanges = fastDeepEqual(sliceOfInterest, metadataChanges); + return !hasChanges; +}; diff --git a/ui-next/src/pages/definition/WorkflowMetadata/state/hook.ts b/ui-next/src/pages/definition/WorkflowMetadata/state/hook.ts new file mode 100644 index 0000000000..56a0f9eb1a --- /dev/null +++ b/ui-next/src/pages/definition/WorkflowMetadata/state/hook.ts @@ -0,0 +1,30 @@ +import { ActorRef } from "xstate"; +import { useSelector } from "@xstate/react"; +import { WorkflowMetadataEvents } from "./types"; + +export const useWorkflowMetadataEditorActor = ( + metadataEditorActor: ActorRef, +) => { + const [nameFieldActor, descriptionFieldActor] = useSelector( + metadataEditorActor, + (state) => state.context.editableFieldActors, + ); + + return [ + { + ownerEmail: useSelector( + metadataEditorActor, + (state) => state.context.metadataChanges?.ownerEmail, + ), + updateTime: useSelector( + metadataEditorActor, + (state) => state.context.metadataChanges?.updateTime, + ), + isDisabled: useSelector(metadataEditorActor, (state) => + state.matches("editingDisabled"), + ), + nameFieldActor, + descriptionFieldActor, + }, + ]; +}; diff --git a/ui-next/src/pages/definition/WorkflowMetadata/state/index.ts b/ui-next/src/pages/definition/WorkflowMetadata/state/index.ts new file mode 100644 index 0000000000..0a5333b576 --- /dev/null +++ b/ui-next/src/pages/definition/WorkflowMetadata/state/index.ts @@ -0,0 +1,4 @@ +export * from "./hook"; +export * from "./machine"; +export * from "./types"; +export * from "./WorkflowMetadataContext"; diff --git a/ui-next/src/pages/definition/WorkflowMetadata/state/machine.ts b/ui-next/src/pages/definition/WorkflowMetadata/state/machine.ts new file mode 100644 index 0000000000..8fca9a3122 --- /dev/null +++ b/ui-next/src/pages/definition/WorkflowMetadata/state/machine.ts @@ -0,0 +1,91 @@ +import { createMachine } from "xstate"; +import * as services from "./services"; + +import * as actions from "./actions"; +import { + WorkflowMetadataMachineEventTypes, + WorkflowMetadataMachineContext, + WorkflowMetadataEvents, +} from "./types"; +import * as guards from "./guards"; +import { LocalCopyMachineEventTypes } from "../../ConfirmLocalCopyDialog/state/types"; +import { createAndDisplayApplicationMachine } from "shared/createAndDisplayApplication/state/machine"; + +export const workflowMetadataMachine = createMachine< + WorkflowMetadataMachineContext, + WorkflowMetadataEvents +>( + { + id: "workflowMetadataEditorMachine", + predictableActionArguments: true, + context: { + metadataChanges: {}, + editableFields: [], + editableFieldActors: [], + childActorsMachineName: "editInPlaceMachine", // Will be provided with the name of the machine to spawn the actors with. + }, + on: { + [LocalCopyMachineEventTypes.USE_LOCAL_COPY_WORKFLOW]: { + actions: ["updateLocalCopy", "notifyActors"], + }, + [WorkflowMetadataMachineEventTypes.FORCE_WORKFLOW]: { + actions: ["updateLocalCopy", "notifyActors"], + cond: "hasMetadataChanges", + }, + }, + type: "parallel", + states: { + fieldEdition: { + initial: "init", + states: { + init: { + entry: "spawnFieldActors", + always: "editingEnabled", + }, + editingEnabled: { + tags: ["editingEnabled"], + on: { + [WorkflowMetadataMachineEventTypes.UPDATE_METADATA]: { + actions: ["persistPartialMetaDataChanges", "syncWithParent"], + }, + [WorkflowMetadataMachineEventTypes.DISABLE_EDITING]: { + target: "editingDisabled", + actions: ["forwardActionToActors"], + }, + }, + }, + editingDisabled: { + tags: ["editingDisabled"], + on: { + [WorkflowMetadataMachineEventTypes.ENABLE_EDITING]: { + target: "editingEnabled", + }, + [WorkflowMetadataMachineEventTypes.WORKFLOW_CHANGED]: { + actions: ["updateLocalCopy", "notifyActors"], + cond: "hasMetadataChanges", + }, + }, + }, + }, + }, + fastApp: { + tags: ["fastAppCreation"], + invoke: { + id: "createAndDisplayApplicationMachine", + src: createAndDisplayApplicationMachine, + data: { + applicationName: (context: WorkflowMetadataMachineContext) => + context.metadataChanges.name, + authHeaders: (context: WorkflowMetadataMachineContext) => + context.authHeaders, + }, + }, + }, + }, + }, + { + actions: actions as any, + guards: guards as any, + services: services as any, + }, +); diff --git a/ui-next/src/pages/definition/WorkflowMetadata/state/services.ts b/ui-next/src/pages/definition/WorkflowMetadata/state/services.ts new file mode 100644 index 0000000000..3df7cc6f9e --- /dev/null +++ b/ui-next/src/pages/definition/WorkflowMetadata/state/services.ts @@ -0,0 +1,81 @@ +import { fetchWithContext } from "plugins/fetch"; +import { WorkflowMetadataMachineContext } from "./types"; +import { getErrorMessage } from "utils/utils"; +// const fetchContext = fetchContextNonHook(); + +export const createApplication = async ( + context: WorkflowMetadataMachineContext, +) => { + const { authHeaders, metadataChanges } = context; + try { + return await fetchWithContext( + "/applications", + {}, + { + method: "POST", + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + body: JSON.stringify({ name: metadataChanges.name }), + }, + ); + } catch (error: any) { + const errorMessage = await getErrorMessage(error); + return { + success: false, + message: errorMessage ?? "Failed to create application", + }; + } +}; + +export const updateApplication = async ( + context: WorkflowMetadataMachineContext, +) => { + const { authHeaders } = context; + const appCreateResponse = await createApplication(context); + const { id } = appCreateResponse; + + const path = `/applications/${id}/roles/UNRESTRICTED_WORKER`; + + try { + await fetchWithContext( + path, + {}, + { + method: "POST", + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + }, + ); + return appCreateResponse; + } catch (error: any) { + const errorMessage = await getErrorMessage(error); + return { + success: false, + message: errorMessage ?? "Failed to create application", + }; + } +}; + +export const generateKeys = async (context: WorkflowMetadataMachineContext) => { + const { authHeaders } = context; + const genApp = await updateApplication(context); + const { id } = genApp; + const path = `/applications/${id}/accessKeys`; + try { + return await fetchWithContext( + path, + {}, + { method: "POST", headers: { ...authHeaders } }, + ); + } catch (error: any) { + const errorMessage = await getErrorMessage(error); + return { + success: false, + message: errorMessage ?? "Failed to generate keys", + }; + } +}; diff --git a/ui-next/src/pages/definition/WorkflowMetadata/state/types.ts b/ui-next/src/pages/definition/WorkflowMetadata/state/types.ts new file mode 100644 index 0000000000..a1e9359fa8 --- /dev/null +++ b/ui-next/src/pages/definition/WorkflowMetadata/state/types.ts @@ -0,0 +1,69 @@ +import { WorkflowDef, WorkflowMetadataI } from "types/WorkflowDef"; +import { ActorRef } from "xstate"; +import { EditInPlaceMachineEvents } from "../EditInPlaceWrapper/state/types"; +import { UseLocalCopyChangesEvent } from "../../ConfirmLocalCopyDialog/state/types"; +import { AuthHeaders } from "types/common"; + +export interface AccessKey { + id: string; + secret: string; +} + +export enum WorkflowMetadataMachineEventTypes { + UPDATE_METADATA = "UPDATE_METADATA", + WORKFLOW_CHANGED = "WORKFLOW_CHANGED", + DISABLE_EDITING = "DISABLE_EDITING", + ENABLE_EDITING = "ENABLE_EDITING", + FORCE_WORKFLOW = "FORCE_WORKFLOW", + CREATE_APPLICATION = "CREATE_APPLICATION", + CLOSE_KEYS_DIALOG = "CLOSE_KEYS_DIALOG", +} + +export interface WorkflowMetadataMachineContext { + metadataChanges: Partial; + editableFields: string[]; + editableFieldActors: ActorRef[]; + childActorsMachineName: "editInPlaceMachine" | "metadataFieldMachine"; + authHeaders?: AuthHeaders; + applicationAccessKey?: AccessKey; +} + +export type UpdateMetaDataEvent = { + type: WorkflowMetadataMachineEventTypes.UPDATE_METADATA; + metadataChanges: Partial; +}; + +export type WorkflowChangedEvent = { + type: WorkflowMetadataMachineEventTypes.WORKFLOW_CHANGED; + workflow: Partial; +}; + +export type ForceWorkflowEvent = { + type: WorkflowMetadataMachineEventTypes.FORCE_WORKFLOW; + workflow: Partial; +}; + +export type DisableEditingEvent = { + type: WorkflowMetadataMachineEventTypes.DISABLE_EDITING; +}; + +export type EnableEditingEvent = { + type: WorkflowMetadataMachineEventTypes.ENABLE_EDITING; +}; + +export type CreateApplicationEvent = { + type: WorkflowMetadataMachineEventTypes.CREATE_APPLICATION; +}; + +export type CloseKeysDialogEvent = { + type: WorkflowMetadataMachineEventTypes.CLOSE_KEYS_DIALOG; +}; +export type WorkflowMetadataEvents = + | UpdateMetaDataEvent + | WorkflowChangedEvent + | EnableEditingEvent + | ForceWorkflowEvent + | DisableEditingEvent + | UseLocalCopyChangesEvent + | CreateApplicationEvent + | CloseKeysDialogEvent; diff --git a/ui-next/src/pages/definition/commonService.ts b/ui-next/src/pages/definition/commonService.ts new file mode 100644 index 0000000000..8de5ae8fc0 --- /dev/null +++ b/ui-next/src/pages/definition/commonService.ts @@ -0,0 +1,70 @@ +import { fetchContextNonHook, fetchWithContext } from "plugins/fetch"; +import { HasAuthHeaders } from "types/common"; +import type { EnvironmentVariables } from "types/EnvVariables"; +import { AUTH_HEADER_NAME, logger } from "utils"; +import { + WORKFLOW_METADATA_BASE_URL, + WORKFLOW_METADATA_SHORT_URL, +} from "utils/constants/api"; +import { queryClient } from "../../queryClient"; + +const fetchContext = fetchContextNonHook(); + +export const refetchAllWorkflowDefinitions = async ({ + authHeaders: headers, +}: HasAuthHeaders) => { + try { + const response = await queryClient.fetchQuery( + [fetchContext.stack, WORKFLOW_METADATA_SHORT_URL], + () => + fetchWithContext(WORKFLOW_METADATA_SHORT_URL, fetchContext, { + headers, + }), + ); + return response; + } catch { + logger.error("Refetching for workflow definitions"); + return Promise.reject({ text: "Unable to fetch for definitions" }); + } +}; + +export const getWorkflowDefinitionByNameAndVersion = async ({ + name, + version, + authHeaders: headers, +}: { + name: string; + version: number; + authHeaders: { [AUTH_HEADER_NAME]?: string }; +}) => { + try { + const path = `${WORKFLOW_METADATA_BASE_URL}/${encodeURIComponent( + name, + )}?version=${version}`; + + return await queryClient.fetchQuery([fetchContext.stack, path], () => + fetchWithContext(path, fetchContext, { headers }), + ); + } catch { + logger.error("Re-fetching for workflow definition by name and version"); + return Promise.reject({ text: "Unable to fetch for definition" }); + } +}; + +export const getEnvVariables = async ({ + authHeaders: headers, +}: HasAuthHeaders): Promise> => { + const url = `/environment`; + try { + const result: EnvironmentVariables[] = await queryClient.fetchQuery( + [fetchContext.stack, url], + () => fetchWithContext(url, fetchContext, { headers }), + ); + return result.reduce( + (acc, { name, value }) => ({ ...acc, [name]: value }), + {}, + ); + } catch { + return {}; + } +}; diff --git a/ui-next/src/pages/definition/confirmSave/ConfirmSaveButtonGroup.tsx b/ui-next/src/pages/definition/confirmSave/ConfirmSaveButtonGroup.tsx new file mode 100644 index 0000000000..ea6c8f539e --- /dev/null +++ b/ui-next/src/pages/definition/confirmSave/ConfirmSaveButtonGroup.tsx @@ -0,0 +1,99 @@ +import Stack from "@mui/material/Stack"; +import { useActor, useSelector } from "@xstate/react"; +import { FunctionComponent } from "react"; +import { ActorRef } from "xstate"; + +import CircularProgress from "@mui/material/CircularProgress"; +import { ButtonTooltip } from "components/ButtonTooltip"; +import SaveIcon from "components/v1/icons/SaveIcon"; +import XCloseIcon from "components/v1/icons/XCloseIcon"; +import { SaveWorkflowEvents, SaveWorkflowMachineEventTypes } from "./state"; +import { useHotkeys } from "react-hotkeys-hook"; +import { Key } from "ts-key-enum"; +import { HOT_KEYS_WORKFLOW_DEFINITION } from "utils/constants/common"; + +interface ConfirmSaveButtonGroupProps { + saveChangesActor: ActorRef; +} + +export const ConfirmSaveButtonGroup: FunctionComponent< + ConfirmSaveButtonGroupProps +> = ({ saveChangesActor }) => { + const [, send] = useActor(saveChangesActor); + + const handleConfirmSaveRequest = () => { + send({ type: SaveWorkflowMachineEventTypes.CONFIRM_SAVE_EVT }); + }; + const handleCancelRequest = () => { + send({ type: SaveWorkflowMachineEventTypes.CANCEL_SAVE_EVT }); + }; + const isSaving = useSelector( + saveChangesActor, + (state) => + state.matches("createWorkflow") || + state.matches("updateWorkflow") || + state.matches("refetchWorkflowDefinitions"), + ); + + // Hotkeys to confirm saving workflow + useHotkeys( + Key.Enter, + (keyboardEvent) => { + keyboardEvent.preventDefault(); + handleConfirmSaveRequest(); + }, + { + scopes: HOT_KEYS_WORKFLOW_DEFINITION, + enableOnFormTags: ["INPUT", "TEXTAREA", "SELECT"], + }, + ); + + // Hotkeys to cancel saving workflow + useHotkeys( + Key.Escape, + (keyboardEvent) => { + keyboardEvent.preventDefault(); + handleCancelRequest(); + }, + { + scopes: HOT_KEYS_WORKFLOW_DEFINITION, + enableOnFormTags: ["INPUT", "TEXTAREA", "SELECT"], + }, + ); + + return ( + + } + > + Cancel + + + } + > + {isSaving ? ( + <> + Saving + + + ) : ( + "Confirm" + )} + + + ); +}; diff --git a/ui-next/src/pages/definition/confirmSave/ConfirmSaveDiffEditor.tsx b/ui-next/src/pages/definition/confirmSave/ConfirmSaveDiffEditor.tsx new file mode 100644 index 0000000000..a2ab5a7a9b --- /dev/null +++ b/ui-next/src/pages/definition/confirmSave/ConfirmSaveDiffEditor.tsx @@ -0,0 +1,73 @@ +import { FunctionComponent, useRef } from "react"; +import { Box } from "@mui/material"; +import { ActorRef } from "xstate"; +import { DiffOnMount, MonacoDiffEditor } from "@monaco-editor/react"; +import { useActor, useSelector } from "@xstate/react"; +import { SaveWorkflowEvents, SaveWorkflowMachineEventTypes } from "./state"; +import { DiffEditor } from "components/DiffEditor/DiffEditor"; + +interface ConfirmSaveDiffEditorProps { + saveChangesActor: ActorRef; + editorTheme: string; + editorState: { + editorOptions: Record; + }; +} + +export const ConfirmSaveDiffEditor: FunctionComponent< + ConfirmSaveDiffEditorProps +> = ({ saveChangesActor, editorTheme, editorState }) => { + const diffMonacoObjects = useRef(null); + const [, send] = useActor(saveChangesActor); + + const handleEditChanges = (changes: string) => + send({ type: SaveWorkflowMachineEventTypes.EDIT_DEBOUNCE_EVT, changes }); + + const isNewWorkflow = useSelector( + saveChangesActor, + (state) => state.context.isNewWorkflow, + ); + const editorChanges = useSelector( + saveChangesActor, + (state) => state.context.editorChanges, + ); + const oldWorkflow = useSelector(saveChangesActor, (state) => + JSON.stringify(state.context.currentWf, null, 2), + ); + + const diffEditorDidMount: DiffOnMount = (editor) => { + diffMonacoObjects.current = editor; + const modifiedEditor = editor.getModifiedEditor(); + modifiedEditor.onDidChangeModelContent((_: any) => { + const maybeText = modifiedEditor.getValue(); + if (typeof maybeText === "string") { + handleEditChanges(maybeText); + } + }); + }; + + return ( + + + + ); +}; diff --git a/ui-next/src/pages/definition/confirmSave/ConfirmWorkflowOverride.tsx b/ui-next/src/pages/definition/confirmSave/ConfirmWorkflowOverride.tsx new file mode 100644 index 0000000000..e379678a6e --- /dev/null +++ b/ui-next/src/pages/definition/confirmSave/ConfirmWorkflowOverride.tsx @@ -0,0 +1,72 @@ +import { useCallback, FunctionComponent } from "react"; +import { useSelector, useActor } from "@xstate/react"; +import { ActorRef } from "xstate"; +import ConfirmChoiceDialog from "components/ConfirmChoiceDialog"; +import { SaveWorkflowEvents, SaveWorkflowMachineEventTypes } from "./state"; +import { Typography } from "components/index"; +import { tryToJson } from "utils/utils"; +import { WorkflowDef } from "types/WorkflowDef"; + +interface ConfirmWorkflowOverrideProps { + saveChangesActor: ActorRef; +} + +export const ConfirmWorkflowOverride: FunctionComponent< + ConfirmWorkflowOverrideProps +> = ({ saveChangesActor }) => { + const [, send] = useActor(saveChangesActor); + const isPromptOverride = useSelector(saveChangesActor, (state) => + state.matches("confirmOverride"), + ); + + const editorChanges = useSelector( + saveChangesActor, + (state) => tryToJson(state.context.editorChanges) as WorkflowDef, + ); + + const handleConfirmOverride = useCallback( + (val: boolean) => + send({ + type: val + ? SaveWorkflowMachineEventTypes.CONFIRM_OVERRIDE_EVT + : SaveWorkflowMachineEventTypes.CANCEL_SAVE_EVT, + } as SaveWorkflowEvents), + [send], + ); + + return isPromptOverride ? ( + + + There seems to be workflow with same name and version. + + + Should we override  + + {editorChanges?.name} + +  ? This cannot be undone. + + + + Please type  + + {editorChanges?.name} + +  to confirm. + + + } + header="Override ?" + isInputConfirmation + valueToBeDeleted={editorChanges?.name} + /> + ) : null; +}; diff --git a/ui-next/src/pages/definition/confirmSave/index.ts b/ui-next/src/pages/definition/confirmSave/index.ts new file mode 100644 index 0000000000..67d2d39c98 --- /dev/null +++ b/ui-next/src/pages/definition/confirmSave/index.ts @@ -0,0 +1,3 @@ +export * from "./ConfirmSaveButtonGroup"; +export * from "./ConfirmSaveDiffEditor"; +export * from "./ConfirmWorkflowOverride"; diff --git a/ui-next/src/pages/definition/confirmSave/state/actions.ts b/ui-next/src/pages/definition/confirmSave/state/actions.ts new file mode 100644 index 0000000000..705f1c73d5 --- /dev/null +++ b/ui-next/src/pages/definition/confirmSave/state/actions.ts @@ -0,0 +1,130 @@ +import _maxBy from "lodash/maxBy"; +import { WorkflowDef } from "types/WorkflowDef"; +import { logger, tryToJson } from "utils"; +import { ActorRef, assign, DoneInvokeEvent, sendParent } from "xstate"; +import { cancel, raise, sendTo } from "xstate/lib/actions"; +import { + ErrorInspectorEventTypes, + ErrorInspectorMachineEvents, +} from "../../errorInspector/state"; +import { + EditEvent, + SaveWorkflowMachineContext, + SaveWorkflowMachineEventTypes, +} from "./types"; + +export const editChanges = assign({ + editorChanges: (_context, { changes }) => changes, +}); + +export const debounceEditEvent = raise( + (__, { changes }) => ({ + type: SaveWorkflowMachineEventTypes.EDIT_EVT, + changes, + }), + { delay: 300, id: "debounce_edit_event" }, +); + +export const cancelDebounceEditChanges = cancel("debounce_edit_event"); + +export const updateWorkflowVersionAndName = assign( + ({ editorChanges }) => { + const workflowJson = tryToJson<{ name: string; version: number }>( + editorChanges, + ); + return { + currentVersion: workflowJson?.version, + workflowName: workflowJson?.name, + }; + }, +); + +export const reportServerErrors = sendTo< + SaveWorkflowMachineContext, + DoneInvokeEvent<{ + text: string; + validationErrors: { message?: string; path?: string }[]; + }>, + ActorRef +>( + ({ errorInspectorMachine }) => errorInspectorMachine!, + (__, { data }) => { + return { + type: ErrorInspectorEventTypes.REPORT_SERVER_ERROR, + text: data.text, + validationErrors: data?.validationErrors, + }; + }, +); + +export const cleanServerErrors = sendTo< + SaveWorkflowMachineContext, + DoneInvokeEvent<{ text: string }>, + ActorRef +>( + ({ errorInspectorMachine }) => errorInspectorMachine!, + (__context, _event) => { + return { + type: ErrorInspectorEventTypes.CLEAN_SERVER_ERRORS, + }; + }, +); + +export const sendSuccessSave = sendParent( + (context) => { + return { + type: SaveWorkflowMachineEventTypes.SAVED_SUCCESSFUL, + workflow: tryToJson(context.editorChanges), // TODO send to errorInspector instead. + isNewWorkflow: false, + workflowName: context.workflowName, + currentVersion: context.currentVersion, + isContinueCreate: context.isContinueCreate, + }; + }, +); + +export const sendCancelSave = sendParent( + (context) => { + try { + const workflow = JSON.parse(context.editorChanges); + + return { + type: SaveWorkflowMachineEventTypes.SAVED_CANCELLED, + workflow, + isNewWorkflow: context.isNewWorkflow, + }; + } catch { + logger.info("Can't parse the json. so returning undefined"); + return { + type: SaveWorkflowMachineEventTypes.SAVED_CANCELLED, + workflow: undefined, + isNewWorkflow: context.isNewWorkflow, + }; + } + }, +); + +export const checkForErrorsInWorkflow = sendTo< + SaveWorkflowMachineContext, + any, + ActorRef +>( + ({ errorInspectorMachine }) => errorInspectorMachine!, + ({ editorChanges }) => ({ + type: ErrorInspectorEventTypes.VALIDATE_WORKFLOW_STRING, + workflowChanges: editorChanges, + }), +); + +export const grabLastVersionAndPersistAsNew = assign< + SaveWorkflowMachineContext, + DoneInvokeEvent> +>({ + currentVersion: (context, { data }) => { + if (data && data.length > 0) { + const latestVersion = _maxBy(data, "version")?.version; + return latestVersion ? latestVersion : context.currentVersion; + } + return context.currentVersion; + }, +}); diff --git a/ui-next/src/pages/definition/confirmSave/state/guards.ts b/ui-next/src/pages/definition/confirmSave/state/guards.ts new file mode 100644 index 0000000000..5cce9125ea --- /dev/null +++ b/ui-next/src/pages/definition/confirmSave/state/guards.ts @@ -0,0 +1,42 @@ +import { logger } from "utils"; +import { DoneInvokeEvent } from "xstate"; +import { SaveWorkflowMachineContext } from "./types"; + +const maybeWorkflowName = (workflowAsString: string): string | null => { + try { + const wf = JSON.parse(workflowAsString); + const { name = null } = wf; + return name; + } catch { + logger.debug("Editor changes is not parsable"); + } + return null; +}; +const maybeWorkflowVersion = (workflowAsString: string): number | null => { + try { + const wf = JSON.parse(workflowAsString); + const { version = null } = wf; + return version; + } catch { + logger.debug("Editor changes is not parsable"); + } + return null; +}; + +export const isNewOrNameChanged = ({ + isNewWorkflow, + currentWf, + editorChanges, +}: SaveWorkflowMachineContext) => + isNewWorkflow || + maybeWorkflowName(editorChanges) !== currentWf.name || + maybeWorkflowVersion(editorChanges) !== currentWf.version; + +export const returnedConflict = ( + _context: SaveWorkflowMachineContext, + { data }: DoneInvokeEvent<{ status: number }>, +) => data.status === 409; + +export const isNewVersion = (_context: SaveWorkflowMachineContext) => { + return _context.isNewVersion; +}; diff --git a/ui-next/src/pages/definition/confirmSave/state/index.ts b/ui-next/src/pages/definition/confirmSave/state/index.ts new file mode 100644 index 0000000000..79fd82215f --- /dev/null +++ b/ui-next/src/pages/definition/confirmSave/state/index.ts @@ -0,0 +1,2 @@ +export * from "./machine"; +export * from "./types"; diff --git a/ui-next/src/pages/definition/confirmSave/state/machine.ts b/ui-next/src/pages/definition/confirmSave/state/machine.ts new file mode 100644 index 0000000000..f8968ac584 --- /dev/null +++ b/ui-next/src/pages/definition/confirmSave/state/machine.ts @@ -0,0 +1,141 @@ +import { createMachine } from "xstate"; +import { + SaveWorkflowMachineEventTypes, + SaveWorkflowEvents, + SaveWorkflowMachineContext, +} from "./types"; + +import * as actions from "./actions"; +import * as guards from "./guards"; +import * as services from "./services"; + +export const saveMachine = createMachine< + SaveWorkflowMachineContext, + SaveWorkflowEvents +>( + { + id: "saveWorkflowMachine", + predictableActionArguments: true, + initial: "confirmSave", + context: { + currentWf: {}, + editorChanges: "", + isNewWorkflow: false, + workflowName: "", + errorInspectorMachine: undefined, + authHeaders: {}, + currentVersion: 1, + isNewVersion: undefined, + isContinueCreate: undefined, + }, + states: { + confirmSave: { + on: { + [SaveWorkflowMachineEventTypes.CONFIRM_SAVE_EVT]: [ + { target: "removeWorkflowFromStorage", cond: "isNewOrNameChanged" }, + { target: "updateWorkflow" }, + ], + [SaveWorkflowMachineEventTypes.CANCEL_SAVE_EVT]: { + target: "savedCancelled", + }, + [SaveWorkflowMachineEventTypes.EDIT_EVT]: { + actions: ["editChanges", "checkForErrorsInWorkflow"], + }, + [SaveWorkflowMachineEventTypes.EDIT_DEBOUNCE_EVT]: { + actions: ["cancelDebounceEditChanges", "debounceEditEvent"], + }, + }, + }, + confirmOverride: { + on: { + [SaveWorkflowMachineEventTypes.CONFIRM_OVERRIDE_EVT]: { + target: "updateWorkflow", + }, + [SaveWorkflowMachineEventTypes.CANCEL_SAVE_EVT]: { + target: "savedCancelled", + }, + }, + }, + createWorkflow: { + invoke: [ + { + src: "createWorkflow", + id: "create-workflow", + onDone: { + actions: ["updateWorkflowVersionAndName"], + target: "refetchWorkflowDefinitions", + }, + onError: [ + { + target: "confirmOverride", + cond: "returnedConflict", + }, + { target: "savedCancelled", actions: ["reportServerErrors"] }, + ], + }, + ], + }, + updateWorkflow: { + invoke: { + src: "updateWorkflow", + id: "update-workflow", + onDone: { + actions: ["updateWorkflowVersionAndName"], + target: "refetchWorkflowDefinitions", + }, + onError: { target: "savedCancelled", actions: "reportServerErrors" }, + }, + }, + removeWorkflowFromStorage: { + invoke: { + src: "removeCopyFromStorage", + onDone: { + target: "createWorkflow", + }, + }, + }, + cleanWorkflowFromStorageAndExit: { + invoke: { + src: "removeCopyFromStorage", + onDone: { + target: "done", + }, + }, + }, + refetchWorkflowDefinitions: { + invoke: { + src: "refetchAllDefinitionsOfCurrentWorkflow", + id: "refetch-all-wf-definitions-of-current-wf", + onError: { target: "confirmSave", actions: "reportServerErrors" }, + onDone: [ + { + cond: "isNewVersion", + actions: [ + "grabLastVersionAndPersistAsNew", + "sendSuccessSave", + "cleanServerErrors", + ], + target: "cleanWorkflowFromStorageAndExit", + }, + { + actions: ["sendSuccessSave", "cleanServerErrors"], + target: "cleanWorkflowFromStorageAndExit", + }, + ], + }, + }, + done: { + type: "final", + }, + savedCancelled: { + entry: "sendCancelSave", + type: "final", + }, + }, + }, + { + actions: actions as any, + guards: guards as any, + services, + }, +); diff --git a/ui-next/src/pages/definition/confirmSave/state/services.ts b/ui-next/src/pages/definition/confirmSave/state/services.ts new file mode 100644 index 0000000000..d9d9f73b2e --- /dev/null +++ b/ui-next/src/pages/definition/confirmSave/state/services.ts @@ -0,0 +1,87 @@ +import { removeCopyFromStorage } from "pages/definition/ConfirmLocalCopyDialog/state"; +import { fetchWithContext } from "plugins/fetch"; +import { WorkflowDef } from "types/WorkflowDef"; +import { SaveWorkflowMachineContext } from "./types"; + +export { removeCopyFromStorage }; + +export const createWorkflow = async ( + { editorChanges, authHeaders }: SaveWorkflowMachineContext, + __: any, +) => { + try { + return await fetchWithContext( + "/metadata/workflow", + {}, + { + method: "POST", + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + + body: editorChanges, + }, + ); + } catch (error: any) { + const errorBody = await error.json(); + return Promise.reject({ + text: errorBody.message, + severity: "error", + status: errorBody.status, + validationErrors: errorBody?.validationErrors, + }); + } +}; + +export const updateWorkflow = async ( + { editorChanges, authHeaders, isNewVersion }: SaveWorkflowMachineContext, + __: any, +) => { + const queryParams = isNewVersion ? "?newVersion=true" : ""; + try { + return await fetchWithContext( + `/metadata/workflow${queryParams}`, + {}, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + body: `[${editorChanges}]`, + }, + ); + } catch (error: any) { + const errorBody = await error.json(); + return Promise.reject({ + text: errorBody.message, + severity: "error", + status: errorBody.status, + validationErrors: errorBody?.validationErrors, + }); + } +}; + +export const refetchAllDefinitionsOfCurrentWorkflow = async ({ + authHeaders: headers, + workflowName, +}: SaveWorkflowMachineContext) => { + const url = `/metadata/workflow?name=${encodeURIComponent(workflowName)}`; + try { + const result: WorkflowDef[] = await fetchWithContext( + url, + {}, + { + method: "GET", + headers: { + "Content-Type": "application/json", + ...headers, + }, + }, + ); + return result; + } catch { + return {}; + } +}; diff --git a/ui-next/src/pages/definition/confirmSave/state/types.ts b/ui-next/src/pages/definition/confirmSave/state/types.ts new file mode 100644 index 0000000000..869d1f4b4b --- /dev/null +++ b/ui-next/src/pages/definition/confirmSave/state/types.ts @@ -0,0 +1,69 @@ +import { ActorRef, DoneInvokeEvent } from "xstate"; +import { ErrorInspectorMachineEvents } from "../../errorInspector/state/types"; +import { WorkflowDef } from "types/WorkflowDef"; + +export enum SaveWorkflowMachineEventTypes { + CONFIRM_SAVE_EVT = "CONFIRM_SAVE", + CANCEL_SAVE_EVT = "CANCEL_SAVE", + EDIT_EVT = "EDIT_EVT", + EDIT_DEBOUNCE_EVT = "CANCEL_DEBOUNCE", + CONFIRM_OVERRIDE_EVT = "CONFIRM_OVERRIDE_EVT", + + SAVED_SUCCESSFUL = "SAVED_SUCCESSFUL", + SAVED_CANCELLED = "SAVED_CANCELLED", +} + +export type ConfirmSaveEvent = { + type: SaveWorkflowMachineEventTypes.CONFIRM_SAVE_EVT; +}; + +export type CancelSaveEvent = { + type: SaveWorkflowMachineEventTypes.CANCEL_SAVE_EVT; +}; + +export type EditEvent = { + type: SaveWorkflowMachineEventTypes.EDIT_EVT; + changes: string; +}; + +export type EditDebounceEvent = { + type: SaveWorkflowMachineEventTypes.EDIT_DEBOUNCE_EVT; + changes: string; +}; + +export type ConfirmOverrideEvent = { + type: SaveWorkflowMachineEventTypes.CONFIRM_OVERRIDE_EVT; +}; + +export type SavedSuccessfulEvent = { + type: SaveWorkflowMachineEventTypes.SAVED_SUCCESSFUL; + workflow: Partial; + isNewWorkflow: boolean; + workflowName: string; + currentVersion: number; +}; + +export type SavedCancelledEvent = { + type: SaveWorkflowMachineEventTypes.SAVED_CANCELLED; + workflowChanges?: Partial; +}; + +export type SaveWorkflowEvents = + | ConfirmSaveEvent + | CancelSaveEvent + | EditEvent + | EditDebounceEvent + | DoneInvokeEvent + | ConfirmOverrideEvent; + +export interface SaveWorkflowMachineContext { + currentWf: Partial; + editorChanges: string; + isNewWorkflow: boolean; + workflowName: string; + authHeaders: Record; + currentVersion: number; + errorInspectorMachine?: ActorRef; + isNewVersion?: boolean; + isContinueCreate?: boolean; +} diff --git a/ui-next/src/pages/definition/errorInspector/AccordionErrorSummary.tsx b/ui-next/src/pages/definition/errorInspector/AccordionErrorSummary.tsx new file mode 100644 index 0000000000..a31cfd41ca --- /dev/null +++ b/ui-next/src/pages/definition/errorInspector/AccordionErrorSummary.tsx @@ -0,0 +1,84 @@ +import { FunctionComponent } from "react"; +import AccordionSummary from "@mui/material/AccordionSummary"; +import { Box, Chip, Stack } from "@mui/material"; +import { CaretRight, CaretDown } from "@phosphor-icons/react"; +import MuiTypography from "components/MuiTypography"; + +interface AccordionErrorSummaryProps { + title: string; + expanded: boolean; + count?: number; +} + +export const AccordionErrorSummary: FunctionComponent< + AccordionErrorSummaryProps +> = ({ title, expanded, count }) => ( + + + {expanded ? ( + + ) : ( + + )} + + + {title} + + {count !== undefined && ( + 1 ? "s" : ""}`} + size="small" + sx={{ + backgroundColor: + title === "Workflow errors" ? "#f44336" : "#ff9800", + color: "white", + fontSize: "0.7rem", + height: 20, + fontWeight: 500, + }} + /> + )} + + + +); diff --git a/ui-next/src/pages/definition/errorInspector/ErrorInspector.tsx b/ui-next/src/pages/definition/errorInspector/ErrorInspector.tsx new file mode 100644 index 0000000000..3ade966152 --- /dev/null +++ b/ui-next/src/pages/definition/errorInspector/ErrorInspector.tsx @@ -0,0 +1,269 @@ +import { Box } from "@mui/material"; +import { CaretUp, Info, WarningCircle, XCircle } from "@phosphor-icons/react"; +import { useMemo } from "react"; +import { ActorRef } from "xstate"; +import ImportSummaryComponent from "./ImportSummary"; +import { ServerErrorsDisplayer } from "./ServerErrorDisplayer"; +import { useErrorInspectorActor } from "./state/hook"; +import { ErrorInspectorMachineEvents } from "./state/types"; +import { TaskErrorsDisplayer } from "./TaskErrorsDisplayer"; +import { WorkflowErrorsDisplayer } from "./WorkflowErrorDisplayer"; + +interface ErrorInspectorProps { + errorInspectorActor: ActorRef; +} + +const ErrorInspector = ({ errorInspectorActor }: ErrorInspectorProps) => { + const [ + { + workflowErrors, + taskErrors, + unreachableTaskErrors, + serverErrors, + errorCount, + taskErrorsExpanded, + workflowErrorsExpanded, + referenceTaskErrorsExpanded, + referenceWorkflowErrorsExpanded, + taskReferenceErrors, + workflowReferenceErrors, + warningCount, + expanded, + tasks, + runWorkflowErrors, + }, + { + handleToggleTaskErrors, + handleToggleWorkflowErrors, + handleCleanServerErrors, + handleToggleTaskReferenceErrors, + handleToggleWorkflowReferenceErrors, + handleClickReference, + handleToggleErrorInspector, + handleJumpToFirstError, + }, + ] = useErrorInspectorActor(errorInspectorActor); + const [statusIcon, barBackgroundColor, problemCount] = useMemo(() => { + const problemCount = errorCount + warningCount; + if (errorCount > 0) { + return [, "#880000", problemCount]; + } + if (warningCount > 0) { + return [ + , + "#c69035", + problemCount, + ]; + } + return [ + , + "#9FDCAA", + problemCount, + ]; + }, [errorCount, warningCount]); + + const handleOnClickReference = (data: string) => { + handleClickReference!(data); + }; + + const textColor = () => { + if (problemCount) { + return "#FFFFFF"; + } + return "#100524"; + }; + const problemLabel = useMemo(() => { + if (problemCount === warningCount) { + return `${problemCount} ${ + warningCount === 1 ? "warning" : "warnings" + } found.`; + } + return `${problemCount} ${ + problemCount === 1 ? "problem" : "problems" + } found.`; + }, [problemCount, warningCount]); + return ( + + + + {/* Left side - Status and message */} + + + {statusIcon} + + + {problemLabel} + + + + {/* Right side - Toggle button */} + + + + + + {expanded ? ( + + + {serverErrors.length > 0 ? ( + handleCleanServerErrors!()} + serverErrors={serverErrors} + onClickReference={handleOnClickReference} + tasks={tasks} + /> + ) : null} + {runWorkflowErrors.length > 0 ? ( + handleCleanServerErrors!()} + serverErrors={runWorkflowErrors} + onClickReference={handleOnClickReference} + tasks={tasks} + /> + ) : null} + {taskErrors.length > 0 ? ( + handleToggleTaskErrors!()} + expanded={taskErrorsExpanded} + /> + ) : null} + {workflowErrors.length > 0 ? ( + handleToggleWorkflowErrors!()} + workflowErrors={workflowErrors} + onClickReference={() => handleJumpToFirstError()} + /> + ) : null} + {unreachableTaskErrors.length > 0 ? ( + handleToggleTaskReferenceErrors!()} + onClickReference={handleOnClickReference} + /> + ) : null} + {taskReferenceErrors.length > 0 ? ( + handleToggleTaskReferenceErrors!()} + title="Task missing references" + onClickReference={handleOnClickReference} + /> + ) : null} + {workflowReferenceErrors.length > 0 ? ( + handleToggleWorkflowReferenceErrors!()} + workflowErrors={workflowReferenceErrors} + title="Workflow missing references" + onClickReference={handleOnClickReference} + /> + ) : null} + + ) : null} + + ); +}; + +export default ErrorInspector; diff --git a/ui-next/src/pages/definition/errorInspector/ImportSummary.tsx b/ui-next/src/pages/definition/errorInspector/ImportSummary.tsx new file mode 100644 index 0000000000..720535cc67 --- /dev/null +++ b/ui-next/src/pages/definition/errorInspector/ImportSummary.tsx @@ -0,0 +1,64 @@ +import { Stack, Typography, List, ListItem } from "@mui/material"; +import { ErrorInspectorMachineEvents } from "./state"; +import { ActorRef } from "xstate"; +import { useSelector } from "@xstate/react"; + +const ImportSummaryComponent = ({ + errorInspectorActor, +}: { + errorInspectorActor: ActorRef; +}) => { + const importSummary = useSelector( + errorInspectorActor, + (state) => state.context.importSummary, + ); + return importSummary == null ? null : ( + + Successfully imported + + {importSummary?.workflowResponse.length > 0 && ( + + + ⚡ Workflows: {importSummary.workflowResponse.length} + + + )} + + {importSummary?.workflowResponse.length > 0 && ( + + + ⚙️ Tasks: {importSummary.workflowResponse.length} + + + )} + + {importSummary?.userFormsResponse.length > 0 && ( + + + 📝 User forms: {importSummary?.userFormsResponse.length} + + + )} + + {importSummary?.schemasResponse.length > 0 && ( + + + 📋 Schemas: {importSummary?.schemasResponse.length} + + + )} + + {importSummary?.integrationsAndModelsResponse.length > 0 && ( + + + 🔌 Integrations:{" "} + {importSummary?.integrationsAndModelsResponse.length} + + + )} + + + ); +}; + +export default ImportSummaryComponent; diff --git a/ui-next/src/pages/definition/errorInspector/ServerErrorDisplayer.tsx b/ui-next/src/pages/definition/errorInspector/ServerErrorDisplayer.tsx new file mode 100644 index 0000000000..81f3eb9c7a --- /dev/null +++ b/ui-next/src/pages/definition/errorInspector/ServerErrorDisplayer.tsx @@ -0,0 +1,203 @@ +import { FunctionComponent } from "react"; +import { ErrorTypes, ValidationError } from "./state/types"; +import { TaskDef } from "types/common"; +import { + AlertTitle, + Alert as MuiAlert, + Box, + Typography, + Chip, +} from "@mui/material"; +import { WarningCircle } from "@phosphor-icons/react"; + +interface ServerErrorsDisplayerProps { + serverErrors: ValidationError[]; + onCleanServerError: () => void; + onClickReference?: (data: string) => void; + tasks?: TaskDef[]; +} + +const titleForServerErrorType = (type: ErrorTypes) => { + switch (type) { + case ErrorTypes.WORKFLOW: + return "Workflow was not saved"; + case ErrorTypes.RUN_ERROR: + return "Could not run workflow"; + default: + return "Error"; + } +}; + +const DEFAULT_TASKS: TaskDef[] = []; + +export const ServerErrorsDisplayer: FunctionComponent< + ServerErrorsDisplayerProps +> = ({ + serverErrors, + tasks = DEFAULT_TASKS, + onCleanServerError, + onClickReference, +}) => { + function extractTaskIndex(input: string): number | null { + const match = input.match(/tasks\[(\d+)\]/); + return match ? parseInt(match[1], 10) : null; + } + + const handleClickValidationError = (path: string) => { + const targetTaskIndex = extractTaskIndex(path); + const taskRefName = + targetTaskIndex != null + ? tasks[targetTaskIndex]?.taskReferenceName + : null; + if (taskRefName && onClickReference) { + onClickReference(`"taskReferenceName": "${taskRefName}"`); + } + }; + + return ( + + {serverErrors?.map(({ message, type, validationErrors }) => ( + + + + {titleForServerErrorType(type)} + + + + + + + {message} + + + + {validationErrors && validationErrors.length > 0 && ( + <> + + + + Validation Errors: + + 1 ? "s" : ""}`} + size="small" + sx={{ + ml: 1, + backgroundColor: "#f44336", + color: "white", + fontSize: "0.7rem", + height: 20, + fontWeight: 500, + }} + /> + + + + {validationErrors?.map((validationError) => ( + + handleClickValidationError(validationError?.path ?? "") + } + > + + {validationError?.message} + + {validationError?.path && ( + + Path: {validationError.path} + + )} + + ))} + + + )} + + ))} + + ); +}; diff --git a/ui-next/src/pages/definition/errorInspector/TaskErrorsDisplayer.tsx b/ui-next/src/pages/definition/errorInspector/TaskErrorsDisplayer.tsx new file mode 100644 index 0000000000..b560f6b658 --- /dev/null +++ b/ui-next/src/pages/definition/errorInspector/TaskErrorsDisplayer.tsx @@ -0,0 +1,312 @@ +import { FunctionComponent, useReducer } from "react"; +import _nth from "lodash/nth"; +import _isArray from "lodash/isArray"; +import { TaskErrors, ValidationError } from "./state/types"; +import { Box, Chip, Typography } from "@mui/material"; +import Accordion from "@mui/material/Accordion"; +import AccordionDetails from "@mui/material/AccordionDetails"; +import Collapse from "@mui/material/Collapse"; +import ListItemButton from "@mui/material/ListItemButton"; +import { AccordionErrorSummary } from "./AccordionErrorSummary"; +import { get } from "lodash"; +import { CaretRight, CaretDown, WarningCircle } from "@phosphor-icons/react"; + +const TaskSingleError: FunctionComponent = ({ + id, + hint, + message, + taskReferenceName, + onClickReference, + taskError, +}) => ( + + + + + Task reference: + + + + + + onClickReference?.(errorRefExtractor(message, taskError))} + > + + Message: + + + {message} + + + + {hint && ( + + + Hint: + + + {hint} + + + )} + +); + +const errorRefExtractor = (string: string, taskError: TaskErrors) => { + let key = _nth(string.split("'"), 1); + if (key !== undefined) { + const value = get(taskError?.task?.inputParameters, key); + if (_isArray(value)) { + return `"${key}"`; + } + if (key.includes(".")) { + key = key.substring(key.lastIndexOf(".") + 1); + } + if (value) { + return `"${key}": "${value}"`; + } + return key; + } + return ""; +}; + +interface TaskGroupedErrorsProps { + taskError: TaskErrors; + onClickReference?: (data: string) => void; + title: string; +} + +const TaskGroupedErrors: FunctionComponent = ({ + taskError, + onClickReference, + title, +}) => { + const [isExpanded, toggleExpand] = useReducer((s) => !s, false); + + function returnTaskReferenceName() { + if (title === "Unreachable tasks") { + const value = _nth(taskError?.errors, 0); + if (value !== undefined) { + return value.taskReferenceName; + } + return "unknown_task"; + } + return taskError.task.taskReferenceName; + } + + const taskName = returnTaskReferenceName(); + const errorCount = taskError.errors.length; + + return ( + + + + + {isExpanded ? ( + + ) : ( + + )} + + + + + {taskName} + + + 1 ? "s" : ""}`} + size="small" + sx={{ + backgroundColor: "#ff9800", + color: "white", + fontSize: "0.7rem", + height: 20, + fontWeight: 500, + }} + /> + + + + + + + {taskError.errors.map((tr) => ( + + + + ))} + + + + ); +}; + +interface TaskErrorsDisplayerProps { + taskErrors: TaskErrors[]; + expanded: boolean; + onToggleExpand: () => void; + title?: string; + onClickReference?: (data: string) => void; +} + +export const TaskErrorsDisplayer: FunctionComponent< + TaskErrorsDisplayerProps +> = ({ + taskErrors, + expanded, + onToggleExpand, + title = "Task Errors", + onClickReference, +}) => { + const totalErrorCount = taskErrors?.reduce( + (sum, taskError) => sum + taskError?.errors?.length, + 0, + ); + + return ( + + + + + {taskErrors.map((taskError) => ( + + ))} + + + + ); +}; diff --git a/ui-next/src/pages/definition/errorInspector/WorkflowErrorDisplayer.tsx b/ui-next/src/pages/definition/errorInspector/WorkflowErrorDisplayer.tsx new file mode 100644 index 0000000000..904d570a7d --- /dev/null +++ b/ui-next/src/pages/definition/errorInspector/WorkflowErrorDisplayer.tsx @@ -0,0 +1,180 @@ +import { FunctionComponent } from "react"; +import { ValidationError } from "./state/types"; +import { Box, Typography } from "@mui/material"; +import Accordion from "@mui/material/Accordion"; +import AccordionDetails from "@mui/material/AccordionDetails"; +import { AccordionErrorSummary } from "./AccordionErrorSummary"; +import { WarningCircle } from "@phosphor-icons/react"; + +const OUTPUT_PARAMETER_REFERENCE = "outputParameters"; + +// Helper component to color words wrapped in double asterisks +const ColoredAsteriskText: FunctionComponent<{ text: string }> = ({ text }) => { + // Regex to match **word** + const regex = /\*\*(.+?)\*\*/g; + const parts = []; + let lastIndex = 0; + let match; + let key = 0; + + while ((match = regex.exec(text)) !== null) { + if (match.index > lastIndex) { + parts.push(text.slice(lastIndex, match.index)); + } + parts.push( + + {match[1]} + , + ); + lastIndex = regex.lastIndex; + } + if (lastIndex < text.length) { + parts.push(text.slice(lastIndex)); + } + return <>{parts}; +}; + +const WorkflowSingleError: FunctionComponent = ({ + hint, + message, + onClickReference, +}) => ( + + + + + Workflow Error: + + + + onClickReference?.(OUTPUT_PARAMETER_REFERENCE)} + > + + Message: + + + + + + + {hint && ( + + + Hint: + + + {hint} + + + )} + +); + +interface WorkflowErrorsDisplayerProps { + workflowErrors: ValidationError[]; + expanded: boolean; + onToggleExpand: () => void; + title?: string; + onClickReference?: (data: string) => void; +} + +export const WorkflowErrorsDisplayer: FunctionComponent< + WorkflowErrorsDisplayerProps +> = ({ + workflowErrors, + expanded, + onToggleExpand, + title = "Workflow errors", + onClickReference, +}) => { + const totalErrorCount = workflowErrors?.length || 0; + + return ( + + + + + {workflowErrors.map((validationError) => ( + + ))} + + + + ); +}; diff --git a/ui-next/src/pages/definition/errorInspector/state/actions.ts b/ui-next/src/pages/definition/errorInspector/state/actions.ts new file mode 100644 index 0000000000..e5c1ebeab7 --- /dev/null +++ b/ui-next/src/pages/definition/errorInspector/state/actions.ts @@ -0,0 +1,395 @@ +import { assign, raise, sendParent, DoneInvokeEvent, choose } from "xstate"; +import { respond } from "xstate/lib/actions"; +import _isEmpty from "lodash/isEmpty"; + +import { adjust, remove } from "utils"; +import { + ValidateWorkflowStringEvent, + ErrorInspectorMachineContext, + ErrorInspectorEventTypes, + WorkflowWithNoErrorsEvent, + WorkflowHasErrorsEvent, + ValidateSingleTaskEvent, + FlowReportedErrorEvent, + ReportServerErrorEvent, + ValidationError, + ErrorIds, + ErrorTypes, + ErrorSeverity, + CleanServerErrorsEvent, + ValidateWorkflowEvent, + ReferenceProblems, + FlowFinishedRenderingEvent, + SetWorkflowEvent, + UpdateSecretsEvent, + ToggleClickReference, + SetErrorInspectorExpandedEvent, + ToggleErrorInspectorEvent, + SetErrorInspectorCollapsedEvent, + ReportRunErrorEvent, + CollapseInspectorIfNoErrorsEvent, + TaskErrors, +} from "./types"; +import { CodeMachineEventTypes } from "pages/definition/EditorPanel/CodeEditorTab/state"; +import { + computeWorkflowStringErrors, + computeWorkflowErrors, + findTaskError, +} from "./schemaValidator"; +import { TaskDef } from "types"; +import { + filterServerErrorsNotPresentInNodes, + nodesToCrumbMap, + reverifyServerErrorsTaskChanges, + serverValidationErrorToIndexTask, +} from "./helpers"; +import { SaveWorkflowMachineEventTypes } from "pages/definition/confirmSave/state/types"; + +export const testForTaskErrors = assign< + ErrorInspectorMachineContext, + ValidateSingleTaskEvent +>(({ taskErrors }, { task }) => { + const ntaskErrors = findTaskError(task); // TODO error checker should check if taskReferenceName exists + const taskIndex = taskErrors.findIndex( + ({ task: { taskReferenceName } }) => + taskReferenceName === task?.taskReferenceName, + ); + if (_isEmpty(ntaskErrors)) { + // no errors. remove entry if exists + return { + taskErrors: + taskIndex === -1 ? taskErrors : remove(taskIndex, 1, taskErrors), + }; + } + // Errors. report the new findings + return { + taskErrors: + taskIndex === -1 + ? taskErrors.concat({ task, errors: ntaskErrors }) + : adjust<{ task: TaskDef; errors: ValidationError[] }>( + taskIndex, + () => ({ + task, + errors: ntaskErrors, + }), + taskErrors, + ), + }; +}); + +export const respondTaskErrors = respond< + ErrorInspectorMachineContext, + ValidateSingleTaskEvent +>(({ taskErrors }) => { + return { + type: ErrorInspectorEventTypes.SINGLE_TASK_ERRORS, + taskErrors, + }; +}); + +export const testForErrors = assign< + ErrorInspectorMachineContext, + ValidateWorkflowEvent +>((_context, { workflow }) => ({ + currentWf: workflow, + ...computeWorkflowErrors(workflow), +})); + +export const verifyChangesInServerErrors = assign< + ErrorInspectorMachineContext, + ValidateWorkflowEvent +>((context, { workflow }) => { + const { serverErrors } = context; + const updatedServerErrors = reverifyServerErrorsTaskChanges( + serverErrors, + workflow, + ); + return { + serverErrors: updatedServerErrors ?? [], + }; +}); + +export const testForErrorsInStringWorkflow = assign< + ErrorInspectorMachineContext, + ValidateWorkflowStringEvent +>(({ serverErrors }, { workflowChanges }) => { + const maybeErrors = computeWorkflowStringErrors(workflowChanges); + if (maybeErrors.currentWf != null) { + const updatedServerErrors = reverifyServerErrorsTaskChanges( + serverErrors, + maybeErrors.currentWf, + ); + return { + ...maybeErrors, + serverErrors: updatedServerErrors ?? [], + }; + } + return { + ...maybeErrors, + }; +}); + +export const notifyErrorFree = sendParent< + ErrorInspectorMachineContext, + WorkflowWithNoErrorsEvent +>(({ currentWf }) => ({ + type: ErrorInspectorEventTypes.WORKFLOW_WITH_NO_ERRORS, + workflow: currentWf, +})); + +export const workflowHasErrors = sendParent< + ErrorInspectorMachineContext, + WorkflowHasErrorsEvent +>(({ taskErrors, workflowErrors, currentWf }) => ({ + type: ErrorInspectorEventTypes.WORKFLOW_HAS_ERRORS, + errors: { + taskErrors, + workflowErrors, + }, + workflow: currentWf, +})); + +export const flowErrorToWorkflowError = assign< + ErrorInspectorMachineContext, + FlowReportedErrorEvent +>({ + workflowErrors: ({ workflowErrors }, { text }) => { + const flowError: ValidationError = { + id: ErrorIds.FLOW_ERROR, + message: text, + hint: "Assert taskReferenceName is not repeated across tasks", + type: ErrorTypes.WORKFLOW, + severity: ErrorSeverity.ERROR, + }; + return workflowErrors.concat(flowError); + }, +}); + +export const removeServerErrorsRelatedToRemovedTasks = assign< + ErrorInspectorMachineContext, + FlowFinishedRenderingEvent +>(({ serverErrors }, { nodes }) => { + const updatedServerErrors = filterServerErrorsNotPresentInNodes( + serverErrors, + nodes, + ); + return { + serverErrors: updatedServerErrors ?? [], + }; +}); + +export const persistServerError = assign< + ErrorInspectorMachineContext, + ReportServerErrorEvent +>(({ currentWf }, { text, validationErrors: incomingValidationErrors }) => { + const validationErrors = incomingValidationErrors ?? [ + { + path: "workflow", + message: text, + }, + ]; // Server error reported without validation. will be treated as a workflow error + const serverError: ValidationError = { + id: ErrorIds.FLOW_ERROR, + message: text, + hint: "Assert taskReferenceName is not repeated across tasks", + type: ErrorTypes.WORKFLOW, + severity: ErrorSeverity.ERROR, + validationErrors: + validationErrors == null + ? undefined + : serverValidationErrorToIndexTask( + validationErrors || [], + currentWf?.tasks || [], + ), + }; + + return { + serverErrors: [serverError], + }; +}); + +export const persistRunError = assign< + ErrorInspectorMachineContext, + ReportRunErrorEvent +>((_, { text }) => { + const runError: ValidationError = { + id: ErrorIds.FLOW_ERROR, + message: text, + hint: "Check run parameters", + type: ErrorTypes.RUN_ERROR, + severity: ErrorSeverity.ERROR, + }; + + return { + runWorkflowErrors: [runError], + }; +}); + +export const persistCrumbMap = assign< + ErrorInspectorMachineContext, + FlowFinishedRenderingEvent +>((_context, { nodes }) => { + return { + crumbMap: nodesToCrumbMap(nodes), + /* currentWf: workflow, */ + }; +}); + +export const persistReferenceProblems = assign< + ErrorInspectorMachineContext, + DoneInvokeEvent +>((_, event) => { + const { data } = event; + return { + lastRemovedTask: undefined, + lastTaskCrumbs: [], + workflowReferenceProblems: data.workflowReferenceProblems, + taskReferencesProblems: data.taskReferencesProblems, + unreachableTaskProblems: data.unreachableTaskProblems, + }; +}); + +export const cleanRunError = assign< + ErrorInspectorMachineContext, + CleanServerErrorsEvent +>({ + runWorkflowErrors: () => [], +}); + +export const cleanServerErrors = assign< + ErrorInspectorMachineContext, + CleanServerErrorsEvent +>({ + serverErrors: () => [], + runWorkflowErrors: () => [], +}); + +export const persistCurrentWorkflow = assign< + ErrorInspectorMachineContext, + SetWorkflowEvent +>({ + currentWf: (__, { workflow }) => workflow, +}); + +export const updateSecretEnvs = assign< + ErrorInspectorMachineContext, + UpdateSecretsEvent +>((_, event) => { + const { data } = event; + return { + secrets: data?.secrets, + envs: data?.envs, + }; +}); + +export const sendReferenceText = sendParent< + ErrorInspectorMachineContext, + ToggleClickReference +>((_, event) => { + return { + type: CodeMachineEventTypes.HIGHLIGHT_TEXT_REFERENCE, + reference: { + textReference: event.referenceText, + referenceReason: "error", + }, + }; +}); + +export const sendJumpToFirstError = sendParent< + ErrorInspectorMachineContext, + ToggleClickReference +>(() => { + return { + type: CodeMachineEventTypes.JUMP_TO_FIRST_ERROR, + }; +}); + +export const toggleErrorInspector = assign< + ErrorInspectorMachineContext, + ToggleErrorInspectorEvent +>({ + expanded: (context) => !context.expanded, +}); + +export const setErrorInspectorExpanded = assign< + ErrorInspectorMachineContext, + SetErrorInspectorExpandedEvent +>({ + expanded: () => true, +}); + +export const setErrorInspectorCollapsed = assign< + ErrorInspectorMachineContext, + SetErrorInspectorCollapsedEvent +>({ + expanded: () => false, +}); + +export const sendCancelConfirmSave = sendParent< + ErrorInspectorMachineContext, + ToggleClickReference +>(() => { + return { + type: SaveWorkflowMachineEventTypes.CANCEL_SAVE_EVT, + }; +}); + +export const raiseExpandErrorInspector = raise< + ErrorInspectorMachineContext, + any +>(() => { + return { + type: ErrorInspectorEventTypes.SET_ERROR_INSPECTOR_EXPANDED, + }; +}); + +export const raiseCollapseErrorInspector = raise< + ErrorInspectorMachineContext, + any +>(() => { + return { + type: ErrorInspectorEventTypes.SET_ERROR_INSPECTOR_COLLAPSED, + }; +}); + +export const raiseCollapseErrorInspectorIfNoErrors = raise< + ErrorInspectorMachineContext, + any +>(() => { + return { + type: ErrorInspectorEventTypes.COLLAPSE_INSPECTOR_IF_NO_ERRORS, + }; +}); + +export const cleanSerializationError = assign({ + workflowErrors: (context) => { + return context.workflowErrors.filter( + (error) => error.id !== ErrorIds.SERIALIZATION_ERROR, + ); + }, +}); + +export const collapseInspectorIfNoErrors = choose< + ErrorInspectorMachineContext, + CollapseInspectorIfNoErrorsEvent +>([ + { + cond: ({ + workflowReferenceProblems, + taskReferencesProblems, + unreachableTaskProblems, + }: ErrorInspectorMachineContext) => { + const taskTotalErrors = taskReferencesProblems.reduce( + (acc: number, { errors }: TaskErrors) => acc + errors.length, + 0, + ); + return ( + workflowReferenceProblems.length + + unreachableTaskProblems.length + + taskTotalErrors === + 0 + ); + }, + actions: [raiseCollapseErrorInspector], + }, +]); diff --git a/ui-next/src/pages/definition/errorInspector/state/helpers.test.ts b/ui-next/src/pages/definition/errorInspector/state/helpers.test.ts new file mode 100644 index 0000000000..b60222ea74 --- /dev/null +++ b/ui-next/src/pages/definition/errorInspector/state/helpers.test.ts @@ -0,0 +1,1056 @@ +import { NodeTaskData } from "components/flow/nodes/mapper/types"; +import { NodeData } from "reaflow"; +import { TaskDef, TaskType } from "types"; +import { CrumbMap } from "types/Crumbs"; +import { + NodeInnerData, + filterServerErrorsNotPresentInNodes, + getVariablesForEachTasks, + jakatraPathToPropertyPath, + nodesToCrumbMap, + reverifyServerErrorsTaskChanges, + serverValidationErrorToIndexTask, +} from "./helpers"; +import { ErrorIds, ErrorSeverity, ErrorTypes } from "./types"; + +export const simpleNodeDiagram = [ + { + id: "start", + text: "start", + ports: [ + { + id: "start-south-port", + width: 2, + height: 2, + side: "SOUTH", + disabled: true, + }, + ], + data: { + task: { + name: "start", + taskReferenceName: "start", + type: "TERMINAL", + }, + crumbs: [], + selected: false, + }, + width: 80, + height: 80, + }, + { + id: "get_random_fact", + text: "get_random_fact", + ports: [ + { + id: "get_random_fact-south-port", + width: 2, + height: 2, + side: "SOUTH", + disabled: true, + }, + ], + data: { + task: { + name: "get_random_fact", + taskReferenceName: "get_random_fact", + inputParameters: { + http_request: { + uri: "https://catfact.ninja/fact", + method: "GET", + connectionTimeOut: 3000, + readTimeOut: 3000, + }, + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + crumbs: [ + { + parent: null, + ref: "get_random_fact", + refIdx: 0, + }, + ], + selected: false, + }, + width: 350, + height: 130, + }, + { + id: "http_lvdn9_ref", + text: "http_lvdn9_ref", + ports: [ + { + id: "http_lvdn9_ref-south-port", + width: 2, + height: 2, + side: "SOUTH", + disabled: true, + }, + ], + data: { + task: { + name: "http_lvdn9_ref", + taskReferenceName: "http_lvdn9_ref", + type: "HTTP", + inputParameters: { + http_request: { + uri: "https://orkes-api-tester.orkesconductor.com/get", + method: "GET", + connectionTimeOut: 3000, + readTimeOut: 3000, + }, + }, + }, + crumbs: [ + { + parent: null, + ref: "get_random_fact", + refIdx: 0, + }, + { + parent: null, + ref: "http_lvdn9_ref", + refIdx: 1, + }, + ], + selected: true, + }, + width: 350, + height: 130, + }, + { + id: "end", + text: "end", + data: { + task: { + name: "end", + taskReferenceName: "end", + type: "TERMINAL", + }, + crumbs: [], + selected: false, + }, + width: 80, + height: 80, + }, +]; +const withExpandedSubWorkflow = [ + { + id: "start", + text: "start", + ports: [ + { + id: "start-south-port", + width: 2, + height: 2, + side: "SOUTH", + disabled: true, + }, + ], + data: { + task: { + name: "start", + taskReferenceName: "start", + type: "TERMINAL", + }, + crumbs: [], + selected: false, + }, + width: 80, + height: 80, + }, + { + id: "get_random_fact", + text: "get_random_fact", + ports: [ + { + id: "get_random_fact-south-port", + width: 2, + height: 2, + side: "SOUTH", + disabled: true, + }, + ], + data: { + task: { + name: "get_random_fact", + taskReferenceName: "get_random_fact", + inputParameters: { + http_request: { + uri: "https://catfact.ninja/fact", + method: "GET", + connectionTimeOut: 3000, + readTimeOut: 3000, + }, + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + crumbs: [ + { + parent: null, + ref: "get_random_fact", + refIdx: 0, + }, + ], + selected: false, + }, + width: 350, + height: 130, + }, + { + id: "sub_workflow_u58mg_ref", + text: "sub_workflow_u58mg_ref", + ports: [ + { + id: "sub_workflow_u58mg_ref-south-port", + width: 2, + height: 2, + side: "SOUTH", + disabled: true, + }, + ], + data: { + task: { + name: "sub_workflow_u58mg_ref", + taskReferenceName: "sub_workflow_u58mg_ref", + inputParameters: {}, + type: "SUB_WORKFLOW", + subWorkflowParam: { + name: "image_convert_resize", + version: 1, + }, + }, + crumbs: [ + { + parent: null, + ref: "get_random_fact", + refIdx: 0, + }, + { + parent: null, + ref: "sub_workflow_u58mg_ref", + refIdx: 1, + }, + ], + selected: true, + }, + width: 350, + height: 100, + }, + { + text: "image_convert_resize", + data: { + task: { + name: "image_convert_resize", + taskReferenceName: "image_convert_resize_ref", + inputParameters: { + fileLocation: "${workflow.input.fileLocation}", + outputFormat: "${workflow.input.recipeParameters.outputFormat}", + outputWidth: "${workflow.input.recipeParameters.outputSize.width}", + outputHeight: "${workflow.input.recipeParameters.outputSize.height}", + }, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + crumbs: [ + { + parent: null, + ref: "get_random_fact", + refIdx: 0, + }, + { + parent: null, + ref: "sub_workflow_u58mg_ref", + refIdx: 1, + }, + { + parent: "sub_workflow_u58mg_ref", + ref: "image_convert_resize_ref", + refIdx: 0, + }, + ], + withinExpandedSubWorkflow: true, + selected: false, + }, + width: 350, + height: 100, + ports: [ + { + id: "image_convert_resize_ref-south-port_swt_image_convert_resize_zwn03", + width: 2, + height: 2, + side: "SOUTH", + disabled: true, + hidden: true, + }, + ], + id: "image_convert_resize_ref_swt_image_convert_resize_zwn03", + parent: "sub_workflow_u58mg_ref", + }, + { + text: "upload_toS3", + data: { + task: { + name: "upload_toS3", + taskReferenceName: "upload_toS3_ref", + inputParameters: { + fileLocation: "${image_convert_resize_ref.output.fileLocation}", + }, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + crumbs: [ + { + parent: null, + ref: "get_random_fact", + refIdx: 0, + }, + { + parent: null, + ref: "sub_workflow_u58mg_ref", + refIdx: 1, + }, + { + parent: "sub_workflow_u58mg_ref", + ref: "image_convert_resize_ref", + refIdx: 0, + }, + { + parent: "sub_workflow_u58mg_ref", + ref: "upload_toS3_ref", + refIdx: 1, + }, + ], + withinExpandedSubWorkflow: true, + selected: false, + }, + width: 350, + height: 100, + ports: [ + { + id: "upload_toS3_ref-south-port_swt_image_convert_resize_zwn03", + width: 2, + height: 2, + side: "SOUTH", + disabled: true, + hidden: true, + }, + ], + id: "upload_toS3_ref_swt_image_convert_resize_zwn03", + parent: "sub_workflow_u58mg_ref", + }, + { + id: "end", + text: "end", + data: { + task: { + name: "end", + taskReferenceName: "end", + type: "TERMINAL", + }, + crumbs: [], + selected: false, + }, + width: 80, + height: 80, + }, +]; + +describe("nodesToCrumbMap", () => { + it("Should return every existing taskReference in diagram", () => { + const result = nodesToCrumbMap( + simpleNodeDiagram as unknown as NodeData[], + ); + expect(Object.keys(result)).toEqual(["get_random_fact", "http_lvdn9_ref"]); + }); + it("Should not include subworkflow child ids if expanded subworkflow", () => { + const result = nodesToCrumbMap( + withExpandedSubWorkflow as unknown as NodeData[], + ); + expect(Object.keys(result)).toEqual([ + "get_random_fact", + "sub_workflow_u58mg_ref", + ]); + }); +}); + +const crumbMaps = { + set_variable_ref: { + task: { + name: "set_variable", + taskReferenceName: "set_variable_ref", + type: "SET_VARIABLE", + inputParameters: { + name: "Orkes", + }, + }, + crumbs: [ + { + parent: null, + ref: "set_variable_ref", + refIdx: 0, + type: "SET_VARIABLE", + }, + ], + }, + simple_ref: { + task: { + name: "simple", + taskReferenceName: "simple_ref", + type: "SIMPLE", + inputParameters: { + "Some-key-kf5rz": "${workflow.variables}", + }, + }, + crumbs: [ + { + parent: null, + ref: "set_variable_ref", + refIdx: 0, + type: "SET_VARIABLE", + }, + { + parent: null, + ref: "simple_ref", + refIdx: 1, + type: "SIMPLE", + }, + ], + }, + set_variable_ref_1: { + task: { + name: "set_variable_1", + taskReferenceName: "set_variable_ref_1", + type: "SET_VARIABLE", + inputParameters: { + year: "2024", + }, + }, + crumbs: [ + { + parent: null, + ref: "set_variable_ref", + refIdx: 0, + type: "SET_VARIABLE", + }, + { + parent: null, + ref: "simple_ref", + refIdx: 1, + type: "SIMPLE", + }, + { + parent: null, + ref: "set_variable_ref_1", + refIdx: 2, + type: "SET_VARIABLE", + }, + ], + }, + join_ref: { + task: { + name: "join", + taskReferenceName: "join_ref", + inputParameters: { + "Some-key-j8nkd": "${workflow.variables.year}", + }, + type: "JOIN", + joinOn: [], + optional: false, + asyncComplete: false, + }, + crumbs: [ + { + parent: null, + ref: "set_variable_ref", + refIdx: 0, + type: "SET_VARIABLE", + }, + { + parent: null, + ref: "simple_ref", + refIdx: 1, + type: "SIMPLE", + }, + { + parent: null, + ref: "set_variable_ref_1", + refIdx: 2, + type: "SET_VARIABLE", + }, + { + parent: null, + ref: "join_ref", + refIdx: 3, + type: "JOIN", + }, + ], + }, +}; +const variablesForTasks = { + set_variable_ref: [], + simple_ref: ["name"], + set_variable_ref_1: ["name"], + join_ref: ["name", "year"], +}; + +const crumbMapsWithoutVariables = { + query_processor_ref: { + task: { + name: "query_processor", + taskReferenceName: "query_processor_ref", + inputParameters: { + workflowNames: [], + statuses: [], + correlationIds: [], + queryType: "CONDUCTOR_API", + startTimeFrom: 60, + startTimeTo: 30, + freeText: "automation test", + }, + type: "QUERY_PROCESSOR", + }, + crumbs: [ + { + parent: null, + ref: "query_processor_ref", + refIdx: 0, + type: "QUERY_PROCESSOR", + }, + ], + }, + http_ref: { + task: { + name: "http", + taskReferenceName: "http_ref", + type: "HTTP", + inputParameters: { + uri: "https://orkes-api-tester.orkesconductor.com/api", + method: "GET", + connectionTimeOut: 3000, + readTimeOut: "3000", + accept: "application/json", + contentType: "application/json", + }, + }, + crumbs: [ + { + parent: null, + ref: "query_processor_ref", + refIdx: 0, + type: "QUERY_PROCESSOR", + }, + { + parent: null, + ref: "http_ref", + refIdx: 1, + type: "HTTP", + }, + ], + }, + inline_ref: { + task: { + name: "inline", + taskReferenceName: "inline_ref", + type: "INLINE", + inputParameters: { + expression: "(function(){ return $.value1 + $.value2;})();", + evaluatorType: "graaljs", + value1: 1, + value2: 2, + }, + }, + crumbs: [ + { + parent: null, + ref: "query_processor_ref", + refIdx: 0, + type: "QUERY_PROCESSOR", + }, + { + parent: null, + ref: "http_ref", + refIdx: 1, + type: "HTTP", + }, + { + parent: null, + ref: "inline_ref", + refIdx: 2, + type: "INLINE", + }, + ], + }, +}; +const variablesForTaskWithoutVariables = { + query_processor_ref: [], + http_ref: [], + inline_ref: [], +}; + +describe("getVariablesForEachTasks", () => { + it("Should return each task with possible references from all taks in crumb with set variable task", () => { + const result = getVariablesForEachTasks(crumbMaps as unknown as CrumbMap); + expect(result).toEqual(variablesForTasks); + }); + it("Should return each task with possible references from all taks in crumb without set variable task", () => { + const result = getVariablesForEachTasks( + crumbMapsWithoutVariables as unknown as CrumbMap, + ); + expect(result).toEqual(variablesForTaskWithoutVariables); + }); +}); + +describe("jakatraPathToPropertyPath", () => { + it("Should return the property path from the jakatra path from a nested fork task", () => { + const result = jakatraPathToPropertyPath( + "update.workflowDefs[0].tasks[1].forkTasks[0].[0]", + ); + expect(result).toEqual("[1].forkTasks[0][0]"); + }); + it("Should return the property path from the jakatra path from a non nested task", () => { + const result = jakatraPathToPropertyPath("update.workflowDefs[0].tasks[1]"); + expect(result).toEqual("[1]"); + }); + it("Should return the property path from a task nested in a switch task decision case", () => { + const result = jakatraPathToPropertyPath( + "update.workflowDefs[0].tasks[1].decisionCases[switch_case].[0]", + ); + expect(result).toEqual("[1].decisionCases[switch_case][0]"); + }); + + it("Should return the property path from a task nested in a switch task default case", () => { + const result = jakatraPathToPropertyPath( + "update.workflowDefs[0].tasks[1].defaultCase[0]", + ); + expect(result).toEqual("[1].defaultCase[0]"); + }); +}); + +describe("serverValidationErrorToIndexMessage", () => { + // Mock TaskDef array for tests + const mockTasks = [ + { name: "task0", taskReferenceName: "task0_ref", type: "SIMPLE" } as any, + { name: "task1", taskReferenceName: "task1_ref", type: "SIMPLE" } as any, + { name: "task2", taskReferenceName: "task2_ref", type: "SIMPLE" } as any, + { name: "task3", taskReferenceName: "task3_ref", type: "SIMPLE" } as any, + ]; + + it("should map validation errors with task indices to IndexMessage objects", () => { + const errors = [ + { path: "update.workflowDefs[0].tasks[0]", message: "Name is required" }, + { path: "update.workflowDefs[0].tasks[2]", message: "Value is invalid" }, + ]; + const result = serverValidationErrorToIndexTask(errors as any, mockTasks); + expect(result).toEqual([ + { + path: "update.workflowDefs[0].tasks[0]", + message: "Name is required", + taskPath: "[0]", + task: mockTasks[0], + }, + { + path: "update.workflowDefs[0].tasks[2]", + message: "Value is invalid", + taskPath: "[2]", + task: mockTasks[2], + }, + ]); + }); + + it("should skip errors without a task index in the path", () => { + const errors = [ + { path: "update.workflowDefs[0]", message: "Name is required" }, + { path: "update.workflowDefs[0].tasks[1]", message: "Value is invalid" }, + ]; + const result = serverValidationErrorToIndexTask(errors as any, mockTasks); + expect(result).toEqual([ + { path: "update.workflowDefs[0]", message: "Name is required" }, + { + path: "update.workflowDefs[0].tasks[1]", + message: "Value is invalid", + taskPath: "[1]", + task: mockTasks[1], + }, + ]); + }); + + it("should handle missing message fields gracefully", () => { + const errors = [{ path: "update.workflowDefs[0].tasks[3]" }]; + const result = serverValidationErrorToIndexTask(errors as any, mockTasks); + expect(result).toEqual([ + { + path: "update.workflowDefs[0].tasks[3]", + taskPath: "[3]", + task: mockTasks[3], + }, + ]); + }); + + it("should return an empty array if no errors have a task index", () => { + const errors = [ + { path: "workflow.input.name", message: "Name is required" }, + { path: "workflow.input.value", message: "Value is invalid" }, + ]; + const result = serverValidationErrorToIndexTask(errors as any, mockTasks); + expect(result).toEqual([ + { path: "workflow.input.name", message: "Name is required" }, + { path: "workflow.input.value", message: "Value is invalid" }, + ]); + }); + + it("should handle paths with only the task index (e.g., 'tasks[0]')", () => { + const errors = [ + { path: "tasks[0]", message: "General error for task 0" }, + { path: "tasks[1]", message: "General error for task 1" }, + ]; + const result = serverValidationErrorToIndexTask(errors as any, mockTasks); + expect(result).toEqual([ + { + path: "tasks[0]", + message: "General error for task 0", + taskPath: "[0]", + task: mockTasks[0], + }, + { + path: "tasks[1]", + message: "General error for task 1", + taskPath: "[1]", + task: mockTasks[1], + }, + ]); + }); + + it("should extract the correct index from paths with prefixes before tasks[]", () => { + const errors = [ + { path: "update.workflowDefs[0].tasks[1]", message: "Error for task 1" }, + { + path: "update.workflowDefs[2].tasks[3]", + message: "Error for task 3", + }, + ]; + const result = serverValidationErrorToIndexTask(errors as any, mockTasks); + expect(result).toEqual([ + { + path: "update.workflowDefs[0].tasks[1]", + message: "Error for task 1", + taskPath: "[1]", + task: mockTasks[1], + }, + { + path: "update.workflowDefs[2].tasks[3]", + message: "Error for task 3", + taskPath: "[3]", + task: mockTasks[3], + }, + ]); + }); +}); + +describe("reverifyServerErrorsTaskChanges", () => { + const mockTask1 = { + name: "task1", + taskReferenceName: "task1_ref", + type: "SIMPLE", + } as any; + const mockTask2 = { + name: "task2", + taskReferenceName: "task2_ref", + type: "SIMPLE", + } as any; + const mockUpdatedTask1 = { ...mockTask1, name: "task1_updated" } as any; + + const createValidationError = (overrides = {}) => ({ + id: ErrorIds.FLOW_ERROR, + message: "Error message", + type: ErrorTypes.WORKFLOW, + severity: ErrorSeverity.ERROR, + hint: "Test hint", + ...overrides, + }); + + it("should filter out validation errors for tasks that have changed", () => { + const serverErrors = [ + createValidationError({ + validationErrors: [ + { + path: "update.workflowDefs[0].tasks[0]", + message: "Error 1", + taskPath: "[0]", + task: mockTask1, + }, + { + path: "update.workflowDefs[0].tasks[1]", + message: "Error 2", + taskPath: "[1]", + task: mockTask2, + }, + ], + }), + ]; + const currentWorkflow = { + tasks: [mockUpdatedTask1, mockTask2], + }; + + const result = reverifyServerErrorsTaskChanges( + serverErrors, + currentWorkflow, + ); + expect(result).toEqual([ + { + ...serverErrors[0], + validationErrors: [ + { + path: "update.workflowDefs[0].tasks[1]", + message: "Error 2", + taskPath: "[1]", + task: mockTask2, + }, + ], + }, + ]); + }); + + it("should return undefined if all validation errors are filtered out", () => { + const serverErrors = [ + createValidationError({ + validationErrors: [ + { + path: "update.workflowDefs[0].tasks[0]", + message: "Error 1", + taskPath: "[0]", + task: mockTask1, + }, + ], + }), + ]; + const currentWorkflow = { + tasks: [mockUpdatedTask1], + }; + + const result = reverifyServerErrorsTaskChanges( + serverErrors, + currentWorkflow, + ); + expect(result).toBeUndefined(); + }); + + it("should handle empty validation errors array", () => { + const serverErrors = [ + createValidationError({ + validationErrors: [], + }), + ]; + const currentWorkflow = { + tasks: [mockTask1], + }; + + const result = reverifyServerErrorsTaskChanges( + serverErrors, + currentWorkflow, + ); + expect(result).toBeUndefined(); + }); + + it("should handle undefined validation errors", () => { + const serverErrors = [createValidationError()]; + const currentWorkflow = { + tasks: [mockTask1], + }; + + const result = reverifyServerErrorsTaskChanges( + serverErrors, + currentWorkflow, + ); + expect(result).toBeUndefined(); + }); + + it("should handle empty server errors array", () => { + const result = reverifyServerErrorsTaskChanges([], { tasks: [mockTask1] }); + expect(result).toBeUndefined(); + }); +}); +describe("filterServerErrorsNotPresentInNodes", () => { + const mockTask1: TaskDef = { + name: "task1", + taskReferenceName: "task1_ref", + type: TaskType.SIMPLE, + startDelay: 0, + joinOn: [], + defaultExclusiveJoinTask: [], + optional: false, + asyncComplete: false, + description: "Mock task 1", + }; + const mockTask2: TaskDef = { + name: "task2", + taskReferenceName: "task2_ref", + type: TaskType.SIMPLE, + startDelay: 0, + joinOn: [], + defaultExclusiveJoinTask: [], + optional: false, + asyncComplete: false, + description: "Mock task 2", + }; + + const createValidationError = (overrides = {}) => ({ + id: ErrorIds.FLOW_ERROR, + message: "Error message", + type: ErrorTypes.WORKFLOW, + severity: ErrorSeverity.ERROR, + hint: "Test hint", + ...overrides, + }); + + const createNode = (task: TaskDef): NodeData> => ({ + id: task.taskReferenceName, + data: { + task, + crumbs: [], + selected: false, + }, + }); + + it("should filter out validation errors for tasks that are not present in nodes", () => { + const serverErrors = [ + createValidationError({ + validationErrors: [ + { + path: "update.workflowDefs[0].tasks[0]", + message: "Error 1", + task: mockTask1, + }, + { + path: "update.workflowDefs[0].tasks[1]", + message: "Error 2", + task: mockTask2, + }, + ], + }), + ]; + const nodes: NodeData>[] = [createNode(mockTask2)]; + + const result = filterServerErrorsNotPresentInNodes(serverErrors, nodes); + expect(result).toEqual([ + { + ...serverErrors[0], + validationErrors: [ + { + path: "update.workflowDefs[0].tasks[1]", + message: "Error 2", + task: mockTask2, + }, + ], + }, + ]); + }); + + it("should return undefined if all validation errors are filtered out", () => { + const serverErrors = [ + createValidationError({ + validationErrors: [ + { + path: "update.workflowDefs[0].tasks[0]", + message: "Error 1", + task: mockTask1, + }, + ], + }), + ]; + const nodes: NodeData>[] = []; + + const result = filterServerErrorsNotPresentInNodes(serverErrors, nodes); + expect(result).toBeUndefined(); + }); + + it("should handle validation errors without task property", () => { + const serverErrors = [ + createValidationError({ + validationErrors: [ + { + path: "update.workflowDefs[0]", + message: "General workflow error", + }, + ], + }), + ]; + const nodes: NodeData>[] = [createNode(mockTask1)]; + + const result = filterServerErrorsNotPresentInNodes(serverErrors, nodes); + expect(result).toEqual([ + { + ...serverErrors[0], + validationErrors: [ + { + path: "update.workflowDefs[0]", + message: "General workflow error", + }, + ], + }, + ]); + }); + + it("should handle empty validation errors array", () => { + const serverErrors = [ + createValidationError({ + validationErrors: [], + }), + ]; + const nodes: NodeData>[] = [createNode(mockTask1)]; + + const result = filterServerErrorsNotPresentInNodes(serverErrors, nodes); + expect(result).toBeUndefined(); + }); + + it("should handle undefined validation errors", () => { + const serverErrors = [createValidationError()]; + const nodes: NodeData>[] = [createNode(mockTask1)]; + + const result = filterServerErrorsNotPresentInNodes(serverErrors, nodes); + expect(result).toBeUndefined(); + }); + + it("should handle empty server errors array", () => { + const result = filterServerErrorsNotPresentInNodes( + [], + [createNode(mockTask1)], + ); + expect(result).toBeUndefined(); + }); +}); diff --git a/ui-next/src/pages/definition/errorInspector/state/helpers.ts b/ui-next/src/pages/definition/errorInspector/state/helpers.ts new file mode 100644 index 0000000000..33b3b143b9 --- /dev/null +++ b/ui-next/src/pages/definition/errorInspector/state/helpers.ts @@ -0,0 +1,273 @@ +import { NodeData } from "reaflow"; +import _last from "lodash/last"; +import { + TaskDef, + TaskType, + Crumb, + CrumbMap, + InlineTaskDef, + DoWhileTaskDef, + JoinTaskDef, + SwitchTaskDef, + JDBCTaskDef, + WorkflowDef, +} from "types"; +import { + extractVariablesFromTask, + undeclaredInputParameters, +} from "pages/definition/helpers"; +import { + ServerValidationError, + StoredValidationError, + ValidationError, +} from "./types"; +import _nth from "lodash/nth"; +import _path from "lodash/fp/path"; + +import fastDeepEqual from "fast-deep-equal"; +import { NodeTaskData } from "components/flow/nodes/mapper"; +export type NodeInnerData = { task: TaskDef; crumbs: Crumb[] }; +type SingleEntry = [string, NodeInnerData]; +type EntriesIgnoreSubWorkflowChilds = { + entries: Array; + subWorkflowTaskReferences: string[]; +}; + +export const nodesToCrumbMap = (nodes: NodeData[]): CrumbMap => { + const entrieWithoutSubWorkflowSubTasks = nodes.reduce( + (acc: EntriesIgnoreSubWorkflowChilds, { id, data, parent }) => { + const taskType = data?.task?.type; + if (taskType === "SWITCH_JOIN") { + return acc; + } + const possibleEntry = [ + [id, { task: data!.task as TaskDef, crumbs: data!.crumbs as Crumb[] }], + ]; + if (taskType === TaskType.SUB_WORKFLOW) { + // If subworkflow extract possible parent + return { + entries: acc.entries.concat(possibleEntry as unknown as SingleEntry), // TS does not seem to know that if a concat an array it will just join it + subWorkflowTaskReferences: acc.subWorkflowTaskReferences.concat(id), + }; + } + if ( + (parent && acc.subWorkflowTaskReferences.includes(parent)) || + id === "start" || + id === "end" + ) { + // if parent is included ignore node and crumbs. we arent drilling on subworkflows so we are safe + return acc; + } + return { + entries: acc.entries.concat(possibleEntry as unknown as SingleEntry), + subWorkflowTaskReferences: acc.subWorkflowTaskReferences, + }; + }, + { entries: [], subWorkflowTaskReferences: [] }, + ); + return Object.fromEntries(entrieWithoutSubWorkflowSubTasks.entries); +}; + +const invalidVariables = (codeExpression: string, givenVariables: string[]) => { + const invalidParameters = givenVariables.map((word: string) => { + const wordRegex = new RegExp(`${word}(?=[^a-zA-Z0-9_$]|$)`, "g"); + const decorators = []; + let _match; + while ((_match = wordRegex.exec(codeExpression))) { + decorators.push(word); + } + return decorators; + }); + return invalidParameters ? invalidParameters.flat() : []; +}; + +export const validateExpressionWithInputParams = ( + task: + | Partial + | Partial + | Partial + | Partial + | Partial, +) => { + if (task.type === TaskType.INLINE) { + const taskExpression = task?.inputParameters?.expression ?? ""; + const addedInputParameters = undeclaredInputParameters( + taskExpression, + task?.inputParameters, + ); + return invalidVariables(taskExpression, addedInputParameters); + } + if (task.type === TaskType.DO_WHILE) { + const taskExpression = task?.loopCondition ?? ""; + const taskReferenceName = task?.taskReferenceName ?? ""; + const addedInputParameters = undeclaredInputParameters( + taskExpression, + task?.inputParameters, + ); + if (addedInputParameters.includes(taskReferenceName)) { + addedInputParameters.splice( + addedInputParameters.indexOf(taskReferenceName), + 1, + ); + } + const loopOverTasks = + task?.loopOver?.map((item) => item.taskReferenceName) ?? []; + const filteredAddedInputParameters = addedInputParameters.filter( + (element) => !loopOverTasks.includes(element), + ); + return invalidVariables(taskExpression, filteredAddedInputParameters); + } + if (task.type === TaskType.SWITCH) { + const taskExpression = task?.expression ?? ""; + const addedInputParameters = undeclaredInputParameters( + taskExpression, + task?.inputParameters, + ); + return invalidVariables(taskExpression, addedInputParameters); + } + if (task.type === TaskType.JOIN) { + const taskExpression = task?.expression ?? ""; + const addedInputParameters = undeclaredInputParameters( + taskExpression, + task?.inputParameters, + ); + let filteredInputParameters = [...addedInputParameters]; + if (addedInputParameters.includes("joinOn")) { + filteredInputParameters = addedInputParameters.filter( + (item) => item !== "joinOn", + ); + } + return invalidVariables(taskExpression, filteredInputParameters); + } + + if (task.type === TaskType.JDBC) { + const taskExpression = task?.inputParameters?.statement ?? ""; + const numberQuestionCharacters = (taskExpression.match(/\?/g) || []).length; + const parameters = task?.inputParameters?.parameters ?? []; + + const isValidParameters = numberQuestionCharacters === parameters.length; + + return isValidParameters + ? [] + : [ + `JDBC task should have ${numberQuestionCharacters} query parameter${ + numberQuestionCharacters > 1 ? "s" : "" + }`, + ]; + } +}; + +export const getVariablesForEachTasks = ( + crumbMaps: CrumbMap, +): Record => { + const referencesForTaskKeys: Record = {}; + Object.entries(crumbMaps).forEach(([key, value]) => { + const tasks = value.crumbs + .map((crumb) => crumb.ref) + .map((ref) => crumbMaps[ref].task); + + const lastTask = _last(tasks); + if (lastTask?.type === TaskType.SET_VARIABLE) { + tasks.pop(); + } + referencesForTaskKeys[key] = extractVariablesFromTask(tasks); + }); + return referencesForTaskKeys; +}; + +export const jakatraPathToPropertyPath = (path?: string): string => { + if (!path) return ""; + + // Extract everything after 'tasks' including the tasks part + const tasksAndAfter = path.split("tasks").pop(); + if (!tasksAndAfter) return ""; + + return ( + tasksAndAfter + // Remove any prefix like update.workflowDefs[0] + .replace(/^.*?tasks/, "tasks") + // Remove markers + .replace(//g, "") + // Remove markers + .replace(//g, "") + // Clean up any double brackets that might have been created + .replace(/\]\[/g, "][") + // Remove any dots that appear right before a bracket + .replace(/\.\[/g, "[") + ); +}; + +export const serverValidationErrorToIndexTask = ( + validationErrors: ServerValidationError[], + workflowTasks: TaskDef[], +): StoredValidationError[] => { + return validationErrors.map((sve) => { + const { path } = sve; + const maybeTaskPath = jakatraPathToPropertyPath(path); + if (maybeTaskPath != null) { + const valAtIdx = _path(maybeTaskPath, workflowTasks); + return valAtIdx != null + ? { + ...sve, + taskPath: maybeTaskPath, + task: valAtIdx, + } + : sve; + } + return sve; + }); +}; + +export const reverifyServerErrorsTaskChanges = ( + serverErrors: ValidationError[], + currentWorkflow: Partial, +): ValidationError[] | undefined => { + const serverError = _nth(serverErrors, 0); + if (serverError != null) { + const validationErrors = + serverError.validationErrors + ?.map((sve) => { + if (sve.path != null && sve?.taskPath == null) return []; // Any change to the workflow means the error is not valid anymore + if (!sve?.taskPath) return sve; + const updatedTask = _path(sve?.taskPath, currentWorkflow.tasks); + if (updatedTask && !fastDeepEqual(sve.task, updatedTask)) { + // task is not the same remove validation + return []; + } + return sve; + }) + .flat() ?? []; + + return validationErrors[0] === undefined + ? undefined + : [{ ...serverError, validationErrors }]; + } +}; + +export const filterServerErrorsNotPresentInNodes = ( + serverErrors: ValidationError[], + nodes: NodeData>[], +) => { + const serverError = _nth(serverErrors, 0); + if (serverError != null) { + const validationErrors = + serverError.validationErrors + ?.map((sve) => { + if (sve?.task == null) return sve; + const targetNode = nodes.find( + (n) => + n.data?.task.taskReferenceName === sve.task?.taskReferenceName, + ); + if (targetNode == null) { + return []; // Node still exist means no changes + } + + return sve; + }) + .flat() ?? []; + + return validationErrors[0] === undefined + ? undefined + : [{ ...serverError, validationErrors }]; + } +}; diff --git a/ui-next/src/pages/definition/errorInspector/state/hook.ts b/ui-next/src/pages/definition/errorInspector/state/hook.ts new file mode 100644 index 0000000000..c2f72b7ca3 --- /dev/null +++ b/ui-next/src/pages/definition/errorInspector/state/hook.ts @@ -0,0 +1,184 @@ +import { ActorRef } from "xstate"; +import { useSelector } from "@xstate/react"; +import { + ErrorInspectorMachineEvents, + ErrorInspectorEventTypes, + TaskErrors, +} from "./types"; + +export const useErrorInspectorActor = ( + errorInspectorActor: ActorRef, +) => { + const send = errorInspectorActor.send; + + const handleToggleTaskErrors = () => { + send({ + type: ErrorInspectorEventTypes.TOGGLE_TASK_ERRORS_VIEWER, + }); + }; + + const handleToggleWorkflowErrors = () => { + send({ + type: ErrorInspectorEventTypes.TOGGLE_WORKFLOW_ERRORS_VIEWER, + }); + }; + + const handleClickReference = (referenceText: string) => { + send({ + type: ErrorInspectorEventTypes.CLICK_REFERENCE, + referenceText, + }); + }; + + const handleJumpToFirstError = () => { + send({ + type: ErrorInspectorEventTypes.JUMP_TO_FIRST_ERROR, + }); + }; + + const handleToggleTaskReferenceErrors = () => { + send({ + type: ErrorInspectorEventTypes.TOGGLE_TASK_REFERENCE_ERRORS_VIEWER, + }); + }; + + const handleToggleWorkflowReferenceErrors = () => { + send({ + type: ErrorInspectorEventTypes.TOGGLE_WORKFLOW_REFERENCE_ERRORS_VIEWER, + }); + }; + + const handleCleanServerErrors = () => { + send({ + type: ErrorInspectorEventTypes.CLEAN_SERVER_ERRORS, + }); + }; + + const handleToggleErrorInspector = () => { + send({ + type: ErrorInspectorEventTypes.TOGGLE_ERROR_INSPECTOR, + }); + }; + + const handleSetErrorInspectorCollapsed = () => { + send({ + type: ErrorInspectorEventTypes.SET_ERROR_INSPECTOR_COLLAPSED, + }); + }; + + return [ + { + workflowErrors: useSelector( + errorInspectorActor, + (state) => state.context.workflowErrors, + ), + taskErrors: useSelector( + errorInspectorActor, + (state) => state.context.taskErrors, + ), + unreachableTaskErrors: useSelector( + errorInspectorActor, + (state) => state.context.unreachableTaskProblems, + ), + serverErrors: useSelector( + errorInspectorActor, + (state) => state.context.serverErrors, + ), + runWorkflowErrors: useSelector( + errorInspectorActor, + (state) => state.context.runWorkflowErrors, + ), + taskReferenceErrors: useSelector( + errorInspectorActor, + (state) => state.context.taskReferencesProblems, + ), + workflowReferenceErrors: useSelector( + errorInspectorActor, + (state) => state.context.workflowReferenceProblems, + ), + errorCount: useSelector( + errorInspectorActor, + ({ + context: { + workflowErrors = [], + taskErrors = [], + serverErrors = [], + runWorkflowErrors = [], + }, + }) => { + const taskTotalErrors = taskErrors.reduce( + (acc: number, { errors }: TaskErrors) => acc + errors.length, + 0, + ); + return ( + workflowErrors.length + + taskTotalErrors + + serverErrors.length + + runWorkflowErrors.length + ); + }, + ), + warningCount: useSelector( + errorInspectorActor, + ({ + context: { + workflowReferenceProblems, + taskReferencesProblems, + unreachableTaskProblems, + }, + }) => { + const taskTotalErrors = taskReferencesProblems.reduce( + (acc: number, { errors }: TaskErrors) => acc + errors.length, + 0, + ); + return ( + workflowReferenceProblems.length + + unreachableTaskProblems.length + + taskTotalErrors + ); + }, + ), + taskErrorsExpanded: useSelector(errorInspectorActor, (state) => + state.matches( + "errorsDisplay.controlledErrors.withErrors.taskErrorsViewer.expanded", + ), + ), + workflowErrorsExpanded: useSelector(errorInspectorActor, (state) => + state.matches( + "errorsDisplay.controlledErrors.withErrors.workflowErrorsViewer.expanded", + ), + ), + referenceTaskErrorsExpanded: useSelector(errorInspectorActor, (state) => + state.matches( + "errorsDisplay.missingReferences.referencesMenus.taskReferences.expanded", + ), + ), + referenceWorkflowErrorsExpanded: useSelector( + errorInspectorActor, + (state) => + state.matches( + "errorsDisplay.missingReferences.referencesMenus.workflowReferences.expanded", + ), + ), + expanded: useSelector( + errorInspectorActor, + (state) => state.context.expanded, + ), + tasks: useSelector( + errorInspectorActor, + (state) => state.context.currentWf?.tasks, + ), + }, + { + handleToggleTaskErrors, + handleToggleWorkflowErrors, + handleCleanServerErrors, + handleToggleTaskReferenceErrors, + handleToggleWorkflowReferenceErrors, + handleClickReference, + handleToggleErrorInspector, + handleSetErrorInspectorCollapsed, + handleJumpToFirstError, + }, + ] as const; +}; diff --git a/ui-next/src/pages/definition/errorInspector/state/index.ts b/ui-next/src/pages/definition/errorInspector/state/index.ts new file mode 100644 index 0000000000..79fd82215f --- /dev/null +++ b/ui-next/src/pages/definition/errorInspector/state/index.ts @@ -0,0 +1,2 @@ +export * from "./machine"; +export * from "./types"; diff --git a/ui-next/src/pages/definition/errorInspector/state/machine.ts b/ui-next/src/pages/definition/errorInspector/state/machine.ts new file mode 100644 index 0000000000..a85491d111 --- /dev/null +++ b/ui-next/src/pages/definition/errorInspector/state/machine.ts @@ -0,0 +1,295 @@ +import { createMachine } from "xstate"; +import { + ErrorInspectorMachineContext, + ErrorInspectorEventTypes, + ErrorInspectorMachineEvents, +} from "./types"; +import * as actions from "./actions"; +import { + testForRemovedTaskReferencesService, + fetchSecretsEndEnvironmentsList, +} from "./service"; + +export const errorInspectorMachine = createMachine< + ErrorInspectorMachineContext, + ErrorInspectorMachineEvents +>( + { + id: "errorInspectorMachine", + predictableActionArguments: true, + initial: "fetchForSecrets", + context: { + currentWf: undefined, + workflowErrors: [], + taskErrors: [], + serverErrors: [], + runWorkflowErrors: [], + crumbMap: undefined, + workflowReferenceProblems: [], + taskReferencesProblems: [], + unreachableTaskProblems: [], + authHeaders: {}, + expanded: false, + }, + on: { + [ErrorInspectorEventTypes.SET_WORKFLOW]: { + actions: ["persistCurrentWorkflow"], + }, + [ErrorInspectorEventTypes.TOGGLE_ERROR_INSPECTOR]: { + actions: ["toggleErrorInspector"], + }, + [ErrorInspectorEventTypes.SET_ERROR_INSPECTOR_EXPANDED]: { + actions: ["setErrorInspectorExpanded", "cleanImportSummary"], + }, + [ErrorInspectorEventTypes.SET_ERROR_INSPECTOR_COLLAPSED]: { + actions: ["setErrorInspectorCollapsed", "cleanImportSummary"], + }, + [ErrorInspectorEventTypes.COLLAPSE_INSPECTOR_IF_NO_ERRORS]: { + actions: ["collapseInspectorIfNoErrors"], + }, + [ErrorInspectorEventTypes.REPORT_SERVER_ERROR]: { + actions: ["persistServerError"], + }, + }, + states: { + fetchForSecrets: { + invoke: { + src: "fetchSecretsEndEnvironmentsList", + id: "fetch-secrets-and-environments", + onDone: { + actions: ["updateSecretEnvs"], + target: "errorsDisplay", + }, + onError: { + target: "errorsDisplay", + }, + }, + }, + errorsDisplay: { + type: "parallel", + states: { + controlledErrors: { + on: { + [ErrorInspectorEventTypes.REPORT_FLOW_ERROR]: { + actions: ["flowErrorToWorkflowError"], + target: ".testState", + }, + [ErrorInspectorEventTypes.VALIDATE_WORKFLOW_STRING]: { + actions: ["testForErrorsInStringWorkflow"], + target: ".testState", + }, + [ErrorInspectorEventTypes.VALIDATE_WORKFLOW]: { + actions: ["testForErrors"], + target: ".testState", + }, + [ErrorInspectorEventTypes.CLEAN_SERIALIZATION_ERROR]: { + actions: [ + "cleanSerializationError", + "raiseCollapseErrorInspectorIfNoErrors", + ], + }, + }, + initial: "idle", + states: { + idle: { + entry: "notifyErrorFree", + }, + withErrors: { + type: "parallel", + entry: "workflowHasErrors", + states: { + taskErrorsViewer: { + initial: "collapsed", + states: { + collapsed: { + on: { + [ErrorInspectorEventTypes.TOGGLE_TASK_ERRORS_VIEWER]: + { + target: "expanded", + }, + }, + }, + expanded: { + on: { + [ErrorInspectorEventTypes.TOGGLE_TASK_ERRORS_VIEWER]: + { + target: "collapsed", + }, + [ErrorInspectorEventTypes.CLICK_REFERENCE]: { + actions: ["setErrorInspectorCollapsed"], + target: "collapsed", + }, + }, + }, + }, + }, + workflowErrorsViewer: { + initial: "collapsed", + states: { + collapsed: { + on: { + [ErrorInspectorEventTypes.TOGGLE_WORKFLOW_ERRORS_VIEWER]: + { + target: "expanded", + }, + }, + }, + expanded: { + on: { + [ErrorInspectorEventTypes.TOGGLE_WORKFLOW_ERRORS_VIEWER]: + { + target: "collapsed", + }, + [ErrorInspectorEventTypes.CLICK_REFERENCE]: { + actions: ["setErrorInspectorCollapsed"], + target: "collapsed", + }, + [ErrorInspectorEventTypes.JUMP_TO_FIRST_ERROR]: { + actions: [ + "sendJumpToFirstError", + "setErrorInspectorCollapsed", + ], + }, + }, + }, + }, + }, + }, + }, + testState: { + always: [ + { + target: "idle", + cond: (context) => { + const { workflowErrors, taskErrors } = context; + return ( + workflowErrors.length === 0 && taskErrors.length === 0 + ); + }, + }, + { target: "withErrors" }, + ], + }, + }, + }, + serverErrors: { + on: { + [ErrorInspectorEventTypes.REPORT_SERVER_ERROR]: { + actions: ["persistServerError", "raiseExpandErrorInspector"], + }, + [ErrorInspectorEventTypes.REPORT_RUN_ERROR]: { + actions: ["persistRunError", "raiseExpandErrorInspector"], + }, + [ErrorInspectorEventTypes.CLEAN_RUN_ERRORS]: { + actions: ["cleanRunError", "raiseCollapseErrorInspector"], + }, + [ErrorInspectorEventTypes.CLEAN_SERVER_ERRORS]: { + actions: [ + "cleanServerErrors", + "raiseCollapseErrorInspectorIfNoErrors", + ], + }, + [ErrorInspectorEventTypes.CLICK_REFERENCE]: { + actions: ["sendCancelConfirmSave", "sendReferenceText"], + }, + [ErrorInspectorEventTypes.VALIDATE_WORKFLOW]: { + actions: [ + "verifyChangesInServerErrors", + "collapseInspectorIfNoErrors", + ], + }, + [ErrorInspectorEventTypes.FLOW_FINISHED_RENDERING]: { + actions: ["removeServerErrorsRelatedToRemovedTasks"], + }, + }, + }, + missingReferences: { + // missingReferences wont prevent the ui from rendering + on: { + [ErrorInspectorEventTypes.FLOW_FINISHED_RENDERING]: { + actions: ["persistCrumbMap"], + target: ".testForMissingReferences", + }, + }, + initial: "referencesMenus", + states: { + testForMissingReferences: { + invoke: { + src: "testForRemovedTaskReferencesService", + id: "testRemovedTaskReferences", + onDone: { + actions: ["persistReferenceProblems"], + target: "referencesMenus", + }, + }, + }, + referencesMenus: { + type: "parallel", + states: { + taskReferences: { + initial: "collapsed", + states: { + collapsed: { + on: { + [ErrorInspectorEventTypes.TOGGLE_TASK_REFERENCE_ERRORS_VIEWER]: + { + target: "expanded", + }, + }, + }, + expanded: { + on: { + [ErrorInspectorEventTypes.TOGGLE_TASK_REFERENCE_ERRORS_VIEWER]: + { + target: "collapsed", + }, + [ErrorInspectorEventTypes.CLICK_REFERENCE]: { + actions: [ + "sendReferenceText", + "setErrorInspectorCollapsed", + ], + }, + }, + }, + }, + }, + workflowReferences: { + initial: "collapsed", + states: { + collapsed: { + on: { + [ErrorInspectorEventTypes.TOGGLE_WORKFLOW_REFERENCE_ERRORS_VIEWER]: + { + target: "expanded", + }, + }, + }, + expanded: { + on: { + [ErrorInspectorEventTypes.TOGGLE_WORKFLOW_REFERENCE_ERRORS_VIEWER]: + { + target: "collapsed", + }, + [ErrorInspectorEventTypes.CLICK_REFERENCE]: { + actions: ["sendReferenceText"], + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + actions: actions as any, + services: { + testForRemovedTaskReferencesService, + fetchSecretsEndEnvironmentsList, + }, + }, +); diff --git a/ui-next/src/pages/definition/errorInspector/state/schemaValidator.ts b/ui-next/src/pages/definition/errorInspector/state/schemaValidator.ts new file mode 100644 index 0000000000..6138a4083b --- /dev/null +++ b/ui-next/src/pages/definition/errorInspector/state/schemaValidator.ts @@ -0,0 +1,305 @@ +import type { ErrorObject } from "ajv"; +import Ajv from "ajv"; +import ajvErrors from "ajv-errors"; +import _path from "lodash/fp/path"; +import _groupBy from "lodash/groupBy"; +import _isEmpty from "lodash/isEmpty"; +import _isNil from "lodash/isNil"; +import { + TaskDef, + WorkflowDef, + schemasByType, + workflowDefinitionSchemaWithDepsAjv, + workflowSchemaAjv, +} from "types"; +import { logger } from "utils"; +import { + ErrorIds, + ErrorSeverity, + ErrorTypes, + SchemaStringValidationResponse, + SchemaValidationResponse, + TaskErrors, + ValidationError, +} from "./types"; + +const taskIndexRegeEx = new RegExp("/tasks/([0-9]{1,})/*"); + +const ajv = new Ajv({ + schemas: workflowDefinitionSchemaWithDepsAjv, + allErrors: true, + allowUnionTypes: true, +}); + +// Ajv option allErrors is required +ajvErrors(ajv /*, {singleError: true} */); + +export const identifyErrorLocation = (validateInstance: any) => { + return _groupBy(validateInstance.errors, ({ instancePath }) => + instancePath.startsWith("/tasks/") ? "workflowTasks" : "workflowRoot", + ); +}; + +export const extractTaskReferenceNameFromTaskErrors = ( + taskErrrors: ErrorObject[], + workflow: Partial, +): TaskDef[] => { + const indexesAndTasks = taskErrrors.reduce((acc, { instancePath }) => { + const match = taskIndexRegeEx.exec(instancePath); + return match != null && match?.length > 1 + ? { ...acc, [match[1]]: workflow.tasks![parseInt(match[1])] } // TODO it is true that its possibly undefined + : acc; + }, {}); + + return Object.values(indexesAndTasks); +}; + +export const truncateToLastNumber = (str: string) => { + // Match any sequence up to the last sequence of numbers + const match = str.match(/^(.*\/\d+)(?:\/|$)/); + return match ? match[1] : str; +}; + +export const convertToPropertyPath = (str: string) => + str + .split("/") + .filter((segment) => segment !== "") // Remove empty segments, e.g., the first one from "/decisionCases/..." + .map((segment) => (isNaN(Number(segment)) ? `.${segment}` : `[${segment}]`)) // Convert to dot or array notation + .join(""); // Join the segments + +const isUnsupportedType = (error: ErrorObject, originalTask: TaskDef) => { + const taskInstancePath = truncateToLastNumber(error.instancePath); + const path = convertToPropertyPath(taskInstancePath); + const maybeTask = _path(path, originalTask); + + return maybeTask?.type in schemasByType === false; +}; + +export const taskErrorToValidationError = ( + errorObj: ErrorObject, + originalTask: TaskDef, +): ValidationError => { + if (_isEmpty(errorObj.instancePath)) { + // Error in outer task + if (errorObj.keyword === "required") { + return { + id: ErrorIds.TASK_REQUIRED_FIELD_MISSING, + message: errorObj?.message ?? "", + hint: `Add the required field ${errorObj?.params?.missingProperty}`, + taskReferenceName: originalTask?.taskReferenceName, + type: ErrorTypes.TASK, + path: errorObj.instancePath, + severity: ErrorSeverity.ERROR, + }; + } + } + + if ( + errorObj.instancePath !== "/name" && + isUnsupportedType(errorObj, originalTask) + ) { + return { + id: ErrorIds.UNKNOWN_TASK_TYPE, + message: errorObj?.message ?? "", + taskReferenceName: originalTask?.taskReferenceName, + path: errorObj.instancePath, + type: ErrorTypes.TASK, + severity: ErrorSeverity.ERROR, + }; + } + + if (errorObj.instancePath.includes("inputParameters")) { + if (errorObj.keyword === "required") { + return { + id: ErrorIds.TASK_REQUIRED_INPUT_PARAMETERS_MISSING, + message: errorObj?.message ?? "", + hint: `Add the required inputParameter ${errorObj?.params?.missingProperty}`, + taskReferenceName: originalTask?.taskReferenceName, + path: errorObj.instancePath, + type: ErrorTypes.TASK, + severity: ErrorSeverity.ERROR, + }; + } + } + + if (errorObj.keyword === "enum") { + return { + id: ErrorIds.ALLOWED_VALUES, + message: errorObj?.message ?? "", + hint: `Use any of the allowed values ${( + errorObj?.params?.allowedValues || [] + ).join(", ")}`, + taskReferenceName: originalTask?.taskReferenceName, + path: errorObj.instancePath, + type: ErrorTypes.TASK, + severity: ErrorSeverity.ERROR, + }; + } + + return { + id: ErrorIds.GENERIC_ERROR, + message: errorObj?.message ?? "", + taskReferenceName: originalTask?.taskReferenceName, + type: ErrorTypes.TASK, + severity: ErrorSeverity.ERROR, + }; +}; + +export const findTaskError = (task: TaskDef): ValidationError[] => { + const taskType = task.type; + + if (_isNil(taskType)) { + return [ + { + id: ErrorIds.TASK_TYPE_NOT_PRESENT, + message: + "Every task should have a type attribute, you seem to have missed the type", + hint: "Add the type: attribute to the task with a supported type.", + type: ErrorTypes.TASK, + + severity: ErrorSeverity.ERROR, + }, + ]; + } + const taskSchema = schemasByType[taskType]; + if (_isNil(taskSchema)) { + return [ + { + id: ErrorIds.UNKNOWN_TASK_TYPE, + message: `Task type ${taskType} is not supported`, + hint: "Use a supported task type", + type: ErrorTypes.TASK, + severity: ErrorSeverity.ERROR, + }, + ]; + } + const validate = ajv.getSchema(taskSchema.$id)!; + const valid = validate(task); + return valid + ? [] + : validate.errors?.map((eo) => taskErrorToValidationError(eo, task)) || []; +}; + +export const createMainValidator = (workflow: Partial): any => { + const validate = ajv.getSchema(workflowSchemaAjv.$id)!; + const valid = validate(workflow); + + return valid ? null : validate; +}; + +export const computeWorkflowStringErrors = ( + workflowString: string, +): SchemaStringValidationResponse => { + try { + const workflow: Partial = JSON.parse(workflowString); + return { + ...computeWorkflowErrors(workflow), + currentWf: workflow, + }; + } catch (err: any) { + const error = err as Error; + logger.info("The error is ", err); + const errorHint = getJSONParseErrorHint(error?.message); + + return { + taskErrors: [], + workflowErrors: [ + { + id: ErrorIds.SERIALIZATION_ERROR, + message: `JSON has a **syntax** error: ${error?.message}`, + hint: errorHint, + type: ErrorTypes.WORKFLOW, + severity: ErrorSeverity.ERROR, + }, + ], + }; + } +}; + +const getJSONParseErrorHint = (errorMessage?: string): string => { + const DEFAULT_ERROR_MESSAGE = + "Workflow definition contains JSON syntax errors. Please check the code tab."; + if (!errorMessage) { + return DEFAULT_ERROR_MESSAGE; + } + + const LOWER_ERROR = errorMessage.toLowerCase(); + + const errorPatterns = { + "unexpected end": + "Workflow definition is incomplete. Please check for missing closing brackets (}) or braces (]) in your JSON.", + "expected property name": + "Malformed workflow definition. Please ensure all property names are properly quoted and check for trailing commas.", + "trailing comma": + "Workflow definition contains invalid trailing commas. Please remove them from objects or arrays.", + "bad control character": + "Invalid character sequence detected in workflow definition. Please check special characters in your JSON strings.", + "invalid escape": + "Invalid character sequence detected in workflow definition. Please check special characters in your JSON strings.", + "duplicate key": + "Workflow definition contains duplicate property names. Each property name in a JSON object must be unique.", + "unexpected number": + "Unexpected number format in workflow definition. Please check for missing quotes around property names or values.", + "expected double-quoted property name": + "Property names in workflow definition must be enclosed in double quotes. Please check your JSON syntax.", + "unterminated string": + "Workflow definition contains an unterminated string. Please check for missing closing quotes in your JSON.", + "unexpected character": + "Unexpected character in workflow definition. Please check for invalid syntax or characters in your JSON.", + }; + + if (LOWER_ERROR.includes("unexpected token") && LOWER_ERROR.includes("'<'")) { + return "Invalid character detected in workflow definition. Please ensure your workflow is defined in valid JSON format."; + } + + if (LOWER_ERROR.includes("unexpected token")) { + return "Syntax error detected in workflow definition. Please check for missing commas, colons, or invalid characters."; + } + + for (const [pattern, hint] of Object.entries(errorPatterns)) { + if (LOWER_ERROR.includes(pattern)) { + return hint; + } + } + + return DEFAULT_ERROR_MESSAGE; +}; + +export const computeWorkflowErrors = ( + workflow: Partial, +): SchemaValidationResponse => { + const mainValidator = createMainValidator(workflow); + if (_isNil(mainValidator)) { + return { + taskErrors: [], + workflowErrors: [], + }; + } + // Group error types + const groupedErrors = identifyErrorLocation(mainValidator); + // Identified workflow errors not related to tasks + const workflowErrors = groupedErrors.workflowRoot || []; + // Tasks containing errors + const tasksWithProblems = extractTaskReferenceNameFromTaskErrors( + groupedErrors.workflowTasks || [], + workflow, + ); + + // Task errors + const taskErrors: TaskErrors[] = tasksWithProblems.reduce( + (acc: TaskErrors[], task: TaskDef) => { + const errors = findTaskError(task).filter( + ({ id }) => id !== ErrorIds.UNKNOWN_TASK_TYPE, // We will filter out this errors + ); + if (errors.length === 0) return acc; + + return [...acc, { task, errors }]; + }, + [], + ); + + return { + taskErrors, + workflowErrors, + }; +}; diff --git a/ui-next/src/pages/definition/errorInspector/state/service.test.ts b/ui-next/src/pages/definition/errorInspector/state/service.test.ts new file mode 100644 index 0000000000..922b80a73f --- /dev/null +++ b/ui-next/src/pages/definition/errorInspector/state/service.test.ts @@ -0,0 +1,599 @@ +import { TaskDef, TaskType, WorkflowDef } from "types"; +import { DEFAULT_WF_ATTRIBUTES } from "utils/constants"; +import { + simpleDiagram, + unknownTaskTypeWf, + workflowWithUnknownType, +} from "../../../../testData/diagramTests"; +import { + computeWorkflowErrors, + convertToPropertyPath, + createMainValidator, + extractTaskReferenceNameFromTaskErrors, + findTaskError, + identifyErrorLocation, + truncateToLastNumber, +} from "./schemaValidator"; +import { + buildInputParameterNotationTree, + createJoinOnReferenceError, + findMissingJoinOnReferences, + findUnMatchedTaskReferences, + isValidNestedVariable, + removedTasksToUnmatchedReferences, + taskReferenceProblemToTaskErrors, + valueContainsTaskReference, + valueContainsVariableTaskReference, + workflowParameterToValidationError, +} from "./service"; +import { ErrorIds } from "./types"; + +describe("Workflow Test", () => { + describe("computeWorkflowErrors", () => { + it("Should return null if no error was found", () => { + const validation = createMainValidator( + simpleDiagram as unknown as WorkflowDef, + ); + expect(validation).toBeNull(); + }); + + it("Should return the validate object on error or null otherwise", () => { + const { name: _noname, ...otherWorkflowProps } = simpleDiagram; + const validation = createMainValidator( + otherWorkflowProps as unknown as WorkflowDef, + ); + expect(validation).not.toBeNull(); + }); + }); + describe("identifyErrorLocation", () => { + it("Should identify workflow as a unique location if the error is within the workflow", () => { + const { name: _noname, ...otherWorkflowProps } = simpleDiagram; + const validation = createMainValidator( + otherWorkflowProps as unknown as WorkflowDef, + ); + const result = identifyErrorLocation(validation); + + expect(result.workflowRoot.length).toBeTruthy(); + }); + it("Should identify workflowTask as a unique location of errors", () => { + const validation = createMainValidator( + workflowWithUnknownType as unknown as WorkflowDef, + ); + const result = identifyErrorLocation(validation); + + expect(result.workflowTasks.length).toBeTruthy(); + }); + it("Should identify two types of error", () => { + const { name: _ignoreName, ...otherWorkflowWithUnknownTypeProps } = + workflowWithUnknownType; + const validation = createMainValidator( + otherWorkflowWithUnknownTypeProps as unknown as WorkflowDef, + ); + const result = identifyErrorLocation(validation); + + expect(result.workflowTasks.length).toBeTruthy(); + expect(result.workflowTasks.length).toBeTruthy(); + }); + }); + + describe("extractTaskReferenceNameFromTaskErrors", () => { + it("Should extract the tasks with the error", () => { + const validation = createMainValidator( + workflowWithUnknownType as unknown as WorkflowDef, + ); + const result = identifyErrorLocation(validation); + + expect(result.workflowTasks.length).toBeTruthy(); + const tasksWithPoblems = extractTaskReferenceNameFromTaskErrors( + result.workflowTasks, + workflowWithUnknownType as unknown as WorkflowDef, + ); + expect(tasksWithPoblems.length).toBe(1); + }); + }); + + describe("findTaskError", () => { + it("Should return an unknown-task-type error", () => { + const taskWithUnknownType = { + name: "image_convert_resize_jim", + taskReferenceName: "image_convert_resize_ref", + inputParameters: {}, + type: "UNKNOWN_TYPE", + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }; + const result = findTaskError(taskWithUnknownType as unknown as TaskDef); + expect(result.length).toBe(1); + expect(result[0].id).toEqual(ErrorIds.UNKNOWN_TASK_TYPE); + }); + + it("Should return an task-type-not-present error", () => { + const taskWithUnknownType = { + name: "image_convert_resize_jim", + taskReferenceName: "image_convert_resize_ref", + inputParameters: {}, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }; + const result = findTaskError(taskWithUnknownType as unknown as TaskDef); + expect(result.length).toBe(1); + expect(result[0].id).toEqual(ErrorIds.TASK_TYPE_NOT_PRESENT); + }); + it("Should return a task-required-field-missing", () => { + const taskWithNoName = { + taskReferenceName: "image_convert_resize_ref", + inputParameters: {}, + type: "SIMPLE", + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }; + const result = findTaskError(taskWithNoName as unknown as TaskDef); + expect(result[0].id).toEqual(ErrorIds.TASK_REQUIRED_FIELD_MISSING); + }); + + it("Should not show taskError with unknown task type", () => { + const result = computeWorkflowErrors(unknownTaskTypeWf as any); + expect(result.taskErrors).toEqual([]); + }); + + // Skipping this test because after refactoring HTTP Task, the http_request is not required in inputParameters + it.skip("Should return a task-required-input-parameter-field", () => { + const httpTask = { + name: "last_task", + taskReferenceName: "last_task", + inputParameters: {}, + type: "HTTP", + }; + const result = findTaskError(httpTask as unknown as TaskDef); + expect(result[0].id).toEqual( + ErrorIds.TASK_REQUIRED_INPUT_PARAMETERS_MISSING, + ); + }); + }); +}); + +describe("findUnMatchedReferences", () => { + it("Should return a list of containing tasks with unmatched references", () => { + const affectedTasks = removedTasksToUnmatchedReferences({ + existingTaskReferences: ["image_convert_resize_ref", "upload_toS3_ref"], + lastTaskRoute: simpleDiagram.tasks as unknown as TaskDef[], + }); + + expect(affectedTasks.length).toBe(1); + }); +}); + +describe("valueContainsTaskReference", () => { + const path = "somePath"; + it("Should return path of reference within a string if no match found", () => { + const result = valueContainsTaskReference( + "${myTestReferenceUnkn.output.fileLocation}", + ["myTestReference"], + path, + ); + expect(result).toEqual([path]); + }); + it("Should return path if there is no reference within a string array", () => { + expect( + valueContainsTaskReference( + ["${myTestReferenceUNK.output.fileLocation}"], + ["myTestReference"], + path, + ), + ).toEqual([path]); + }); + it("Should return path with nested key if there is no reference within an object value", () => { + expect( + valueContainsTaskReference( + { a: "${myTestReference.output.fileLocation}" }, + ["myTest"], + path, + ), + ).toEqual([`${path}.a`]); + }); + + it("Should return path if there is no reference within a nested object value", () => { + expect( + valueContainsTaskReference( + { a: { b: "${myTestReference.output.fileLocation}" } }, + ["myTest"], + path, + ), + ).toEqual([`${path}.a.b`]); + }); + + it("Should return empty if there is no reference to task inLocation", () => { + expect( + valueContainsTaskReference( + { + a: { b: "${myTestReferenceThat does not exist.output.fileLocation}" }, + }, + ["myTestReference"], + "i", + ), + ).toEqual(["i.a.b"]); + }); + it("Should return path if extra space found", () => { + const result = valueContainsTaskReference( + "${ myTestReference.output }", + ["myTestReference"], + path, + ); + expect(result).toEqual([path]); + }); + it("Should pass the check if [] found", () => { + const result = valueContainsTaskReference( + "${myTestReference.output.test[0]}", + ["myTestReference"], + path, + ); + expect(result).toEqual([]); + }); + it("Should return path if nested ${} found", () => { + const result = valueContainsTaskReference( + "${${myTestReference.output}}", + ["myTestReference"], + path, + ); + expect(result).toEqual([path]); + }); + it("Should pass if single hyphen found", () => { + const result = valueContainsTaskReference( + "${myTestReference.output-iid}", + ["myTestReference"], + path, + ); + expect(result).toEqual([]); + }); + it("Should return path if sequence of hyphen found", () => { + const result = valueContainsTaskReference( + "${myTestReference.output--iid}", + ["myTestReference"], + path, + ); + expect(result).toEqual([path]); + }); + it("Should return path if special characters found", () => { + const result = valueContainsTaskReference( + "${myTestReference.output#?@%&}", + ["myTestReference"], + path, + ); + expect(result).toEqual([path]); + }); + it("Should return path if space inbetween", () => { + const result = valueContainsTaskReference( + "${myTestReference .output}", + ["myTestReference"], + path, + ); + expect(result).toEqual([path]); + }); + it("Should return legacy error if ${CPEWF_TASK_ID} found(workflow missing references)", () => { + const result = workflowParameterToValidationError({ + data: "${CPEWF_TASK_ID}", + workflowName: "some_workflow", + }); + const expectedMessage = + "'data' references '${CPEWF_TASK_ID}', is a legacy ref and should be replaced by 'task_ref_name.taskId'"; + expect(result.message).toEqual(expectedMessage); + }); + it("Should return legacy error if ${CPEWF_TASK_ID} found(task missing references)", () => { + const result = taskReferenceProblemToTaskErrors({ + task: { + name: "get_random_fact", + taskReferenceName: "get_random_fact", + description: "", + startDelay: 0, + joinOn: [""], + optional: false, + defaultExclusiveJoinTask: [""], + inputParameters: { + http_request: { + uri: "https://orkes-api-tester.orkesconductor.com/api", + method: "GET", + connectionTimeOut: 3000, + readTimeOut: 3000, + accept: "application/json", + contentType: "application/json", + body: "${CPEWF_TASK_ID}", + }, + }, + type: TaskType.HTTP, + }, + parameters: ["http_request.body"], + expressions: [], + }); + + const expectedMessage = + "input parameter 'http_request.body' references '${CPEWF_TASK_ID}', A legacy ref and should be replaced by 'task_ref_name.taskId'"; + expect(result.errors[0]?.message).toEqual(expectedMessage); + }); + it("Should pass if default workflow variables are found", () => { + const taskReferences = [ + "workflow.workflowId", + "workflow.output", + "workflow.status", + "workflow.parentWorkflowId", + "workflow.parentWorkflowTaskId", + "workflow.workflowType", + "workflow.version", + "workflow.correlationId", + "workflow.variables", + "workflow.createTime", + "workflow.taskToDomain", + ]; + DEFAULT_WF_ATTRIBUTES.forEach((item) => { + const result = valueContainsTaskReference( + `\${${item}}`, + taskReferences, + path, + ); + expect(result).toEqual([]); + }); + }); +}); + +describe("valueContainsVariableTaskReference", () => { + const path = "somePath"; + const stringValue = "${workflow.variables.count}"; + const noRefValue = "${workflow.variables.something}"; + const nestedArray = [ + "${workflow.variables.name}", + "${workflow.variables.types}", + ["${workflow.variables.type}", "${workflow.variables.type}"], + ]; + const nestedObject = { + value1: "${workflow.variables.year}", + value2: "${workflow.variables.location}", + value3: { + innerValue1: "${workflow.variables.years}", + innerValue2: "${workflow.variables.location}", + }, + }; + const variableReferences = [ + "workflow.variables.name", + "workflow.variables.type", + "workflow.variables.year", + "workflow.variables.location", + "workflow.variables.count", + ]; + + it("Should return empty if there is no reference to task inLocation", () => { + expect( + valueContainsVariableTaskReference(stringValue, variableReferences, path), + ).toEqual([]); + }); + it("Should return path of reference within a string if no match found", () => { + const result = valueContainsVariableTaskReference( + noRefValue, + variableReferences, + path, + ); + expect(result).toEqual([path]); + }); + it("Should return path if there is no reference within a nested array", () => { + expect( + valueContainsVariableTaskReference(nestedArray, variableReferences, path), + ).toEqual([path]); + }); + it("Should return path with nested key if there is no reference within an nested object", () => { + expect( + valueContainsVariableTaskReference( + nestedObject, + variableReferences, + path, + ), + ).toEqual([`${path}.value3.innerValue1`]); + }); + + it("Should show taskReferences with list of pathsAffected", () => { + const affectedTask = { + name: "upload_toS3_jim", + taskReferenceName: "upload_toS3_ref", + inputParameters: { + fileLocation: "${image_convert_resize_ref.output.fileLocation}", + }, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + } as unknown as TaskDef; + const result = findUnMatchedTaskReferences( + ["upload_toS3_ref"], + [affectedTask], + ); + expect(result).toEqual( + expect.arrayContaining([ + { + task: affectedTask, + parameters: ["fileLocation"], + expressions: [], + joinOn: [], + }, + ]), + ); + }); +}); + +describe("buildInputParameterNotationTree", () => { + const path = "somePath"; + it("Should return path if value is just a string", () => { + const result = buildInputParameterNotationTree({ a: "justAString" }, path); + expect(result).toEqual([path + ".a"]); + }); + it("Should return path if value is an array", () => { + expect( + buildInputParameterNotationTree( + { a: ["${myTestReferenceUNK.output.fileLocation}"] }, + path, + ), + ).toEqual([path + ".a"]); + }); + + it("Should return path if value is in a nested object", () => { + expect( + buildInputParameterNotationTree( + { a: { b: "${myTestReference.output.fileLocation}" } }, + path, + ), + ).toEqual([`${path}.a.b`]); + }); + it("Should return empty if provided object is empty", () => { + expect(buildInputParameterNotationTree({}, path)).toEqual([]); + }); + it("Should return two paths even if one is empty", () => { + expect(buildInputParameterNotationTree({ a: "some", b: {} }, path)).toEqual( + [`${path}.a`, `${path}.b`], + ); + }); +}); + +describe("truncateToLastNumber", () => { + it("Should return path to last number", () => { + expect( + truncateToLastNumber("/decisionCases/new_case_bmoty/0/inputParameters"), + ).toEqual("/decisionCases/new_case_bmoty/0"); + }); + it("Should support more than one path", () => { + expect( + truncateToLastNumber( + "/decisionCases/new_case_bmoty/0/decsionCases/otherPath/1/type", + ), + ).toEqual("/decisionCases/new_case_bmoty/0/decsionCases/otherPath/1"); + }); +}); + +describe("convertToPropertyPath", () => { + it("Should take an instancePath and convert it to a property path", () => { + const instancePath = "/decisionCases/new_case_bmoty/0/inputParameters"; + const longInstancePath = + "/decisionCases/new_case_bmoty/0/decsionCases/otherPath/1"; + expect(convertToPropertyPath(instancePath)).toEqual( + ".decisionCases.new_case_bmoty[0].inputParameters", + ); + expect(convertToPropertyPath(longInstancePath)).toEqual( + ".decisionCases.new_case_bmoty[0].decsionCases.otherPath[1]", + ); + }); +}); + +describe("isValidNestedVariable", () => { + const expectedReferences = [ + "workflow.input", + "workflow.input.test", + "workflow.secrets", + ]; + it("Should return true as variables nested are available", () => { + const valueString = "${workflow.secrets.${workflow.input.test}}"; + expect(isValidNestedVariable(expectedReferences, valueString)).toEqual( + true, + ); + }); + it("Should return false as some variable is not available", () => { + const valueString1 = "${workflow.secrets.${workflow.input.cool}}"; + expect(isValidNestedVariable(expectedReferences, valueString1)).toEqual( + false, + ); + }); + it("Should return true - nested variables inside the url", () => { + const valueString3 = + "https://orkes-api-tester.orkesconductor.com/api/${workflow.secrets.${workflow.input.test}}"; + expect(isValidNestedVariable(expectedReferences, valueString3)).toEqual( + true, + ); + }); +}); + +describe("findMissingJoinOnReferences", () => { + const baseTask = { + name: "dummy", + taskReferenceName: "dummy_ref", + description: "", + startDelay: 0, + inputParameters: {}, + optional: false, + asyncComplete: false, + defaultExclusiveJoinTask: [], + loopOver: [], + decisionCases: {}, + defaultCase: [], + forkTasks: [], + type: undefined, + joinOn: [], + }; + + const task = { + ...baseTask, + name: "join", + taskReferenceName: "join_ref", + type: TaskType.JOIN, + joinOn: ["a", "b", "c"], + }; + it("returns missing joinOn references", () => { + expect(findMissingJoinOnReferences(task, ["a", "c"])).toEqual(["b"]); + }); + + it("returns empty array if all joinOn references exist", () => { + const taskAllExist = { + ...baseTask, + type: TaskType.JOIN, + joinOn: ["a", "b"], + }; + expect(findMissingJoinOnReferences(taskAllExist, ["a", "b"])).toEqual([]); + }); + + it("returns empty array if not a JOIN task", () => { + const notJoinTask = { + ...baseTask, + type: TaskType.SIMPLE, + joinOn: ["a", "b"], + }; + expect(findMissingJoinOnReferences(notJoinTask, ["a", "b"])).toEqual([]); + }); +}); + +describe("createJoinOnReferenceError", () => { + const baseTask = { + name: "dummy", + taskReferenceName: "dummy_ref", + description: "", + startDelay: 0, + inputParameters: {}, + optional: false, + asyncComplete: false, + defaultExclusiveJoinTask: [], + loopOver: [], + decisionCases: {}, + defaultCase: [], + forkTasks: [], + type: undefined, + joinOn: [], + }; + it("returns a structured error for missing joinOn reference", () => { + const task = { + ...baseTask, + name: "join", + taskReferenceName: "join_ref", + type: TaskType.JOIN, + joinOn: ["a", "b", "c"], + }; + const missingRef = "missing_task"; + expect(createJoinOnReferenceError(task, missingRef)).toEqual({ + id: "reference-problems", + taskReferenceName: "join_ref", + message: "joinOn references missing taskReferenceName 'missing_task'", + type: "TASK", + severity: "WARNING", + }); + }); +}); diff --git a/ui-next/src/pages/definition/errorInspector/state/service.ts b/ui-next/src/pages/definition/errorInspector/state/service.ts new file mode 100644 index 0000000000..651197684d --- /dev/null +++ b/ui-next/src/pages/definition/errorInspector/state/service.ts @@ -0,0 +1,539 @@ +import _entries from "lodash/entries"; +import _first from "lodash/first"; +import _isArray from "lodash/isArray"; +import _isEmpty from "lodash/isEmpty"; +import _isObject from "lodash/isObject"; +import _nth from "lodash/nth"; +import { getEnvVariables } from "pages/definition/commonService"; +import { fetchContextNonHook, fetchWithContext } from "plugins/fetch"; +import { queryClient } from "queryClient"; +import { InlineTaskDef, TaskDef, TaskType } from "types"; +import { WorkflowDef } from "types/WorkflowDef"; +import { DEFAULT_WF_ATTRIBUTES } from "utils/constants"; +import { FEATURES, featureFlags } from "utils/flags"; +import { + getVariablesForEachTasks, + validateExpressionWithInputParams, +} from "./helpers"; +import { + ErrorIds, + ErrorInspectorMachineContext, + ErrorSeverity, + ErrorTypes, + ReferenceProblems, + RefractorObject, + TaskErrors, + TaskReferenceReportingParameters, + TaskWithUnknownReference, + ValidationError, +} from "./types"; + +const fetchContext = fetchContextNonHook(); + +const enabledAdvancedValidations = featureFlags.isEnabled( + FEATURES.ADVANCED_ERROR_INSPECTOR_VALIDATIONS, +); + +export const valueContainsTaskReference = ( + value: any, + taskReferences: string[], + path = "", +): string[] => { + if (typeof value === "string") { + const hasReference = taskReferences.some( + (tr) => + value.includes(`{${tr}.`) || + value.includes(`{${tr}}`) || + value.includes(`{workflow.input}`) || + value.includes(`{workflow.secrets}`) || + value.includes(`{workflow.env}`), + ); + // this regex will allow letters,digits,underscore,dot and hyphen + const pattern = + /\${(?!.*(\$|\{|}|<>|-)\1)[a-zA-Z_\d-[\]]+(\.[a-zA-Z_\d-[\]]+)*(\.[a-zA-Z_]+\(\))?}/; + + const expectedReferences = [ + ...taskReferences, + "workflow.input", + "workflow.secrets", + "workflow.env", + ]; + + const isValidVariable = enabledAdvancedValidations + ? pattern.test(value) || isValidNestedVariable(expectedReferences, value) + : pattern.test(value); + + return value.includes("${") && (!hasReference || !isValidVariable) + ? [path] + : []; + } + if (_isArray(value)) { + return value.flatMap((v) => + valueContainsTaskReference(v, taskReferences, path), + ); + } + + if (_isObject(value)) { + return Object.entries(value).flatMap( + ([key, value]) => + valueContainsTaskReference(value, taskReferences, `${path}.${key}`), + 0, + ); + } + + return []; +}; + +export const valueContainsVariableTaskReference = ( + value: any, + taskReferences: string[], + path = "", +): string[] => { + if (typeof value === "string" && value.startsWith("${workflow.variables.")) { + const hasReference = taskReferences.some( + (tr) => value.includes(`{${tr}.`) || value.includes(`{${tr}}`), + ); + const pattern = + /\${(?!.*(\$|\{|}|<>|-)\1)[a-zA-Z_\d-[\]]+(\.[a-zA-Z_\d-[\]]+)*(\.[a-zA-Z_]+\(\))?}/; + + const isValidVariable = pattern.test(value); + + return value.includes("${") && isValidVariable && !hasReference + ? [path] + : []; + } + if (_isArray(value)) { + return value.flatMap((v) => + valueContainsVariableTaskReference(v, taskReferences, path), + ); + } + + if (_isObject(value)) { + return Object.entries(value).flatMap( + ([key, value]) => + valueContainsVariableTaskReference( + value, + taskReferences, + `${path}.${key}`, + ), + 0, + ); + } + + return []; +}; + +export const findTaskReferencesInInputParameters = ( + existingTaskReferences: string[], + task: Partial, +): string[] => { + const taskInputParameters = task?.inputParameters || {}; + return Object.entries(taskInputParameters).flatMap(([key, value]) => + valueContainsTaskReference(value, existingTaskReferences, key), + ); +}; + +export const findMissingJoinOnReferences = ( + task: TaskDef, + existingTaskReferences: string[], +): string[] => { + if (task.type === TaskType.JOIN && Array.isArray(task.joinOn)) { + return task.joinOn.filter((ref) => !existingTaskReferences.includes(ref)); + } + return []; +}; + +export const findUnMatchedTaskReferences = ( + existingTaskReferences: string[], + possiblyAffectedTasks: TaskDef[], +): TaskWithUnknownReference[] => { + return possiblyAffectedTasks.reduce( + (acc: TaskWithUnknownReference[], task): TaskWithUnknownReference[] => { + const parameters = findTaskReferencesInInputParameters( + existingTaskReferences, + task, + ); + const expressions = + validateExpressionWithInputParams(task as Partial) ?? []; + + const joinOn = findMissingJoinOnReferences(task, existingTaskReferences); + + if (_isEmpty(parameters) && _isEmpty(expressions) && _isEmpty(joinOn)) { + return acc; + } + return acc.concat({ + task, + parameters, + expressions, + joinOn, + }); + }, + [], + ); +}; + +export const findUnMatchedWorkflowReferences = ( + existingTaskReferences: string[], + workflow: Partial, +) => { + const workflowOutputParams = workflow?.outputParameters || {}; + + const unMatchedWorkflowReferences = Object.entries( + workflowOutputParams, + ).reduce((acc: string[], [k, v]) => { + const affectedParams = valueContainsTaskReference( + v, + existingTaskReferences, + k, + ); + if (_isEmpty(affectedParams)) { + return acc; + } + return acc.concat(affectedParams); + }, []); + + let refractoredArray: RefractorObject[] = []; + if (workflow && workflow.outputParameters) { + refractoredArray = findObjectWithValue( + unMatchedWorkflowReferences, + workflow, + ); + } + + return refractoredArray; +}; + +export const findVariableReferencesInInputParameters = ( + variableTaskReferences: Record, + task: Partial, +) => { + const taskInputParameters = task?.inputParameters || {}; + const currentTaskVariable = + (task.taskReferenceName && + variableTaskReferences[task?.taskReferenceName]) || + []; + const possibleVariablePaths = currentTaskVariable.map( + (variable) => `workflow.variables.${variable}`, + ); + return Object.entries(taskInputParameters).flatMap(([key, value]) => + valueContainsVariableTaskReference(value, possibleVariablePaths, key), + ); +}; + +export const findUnMatchedVariableReferencesTaks = ( + variableTaskReferences: Record, + tasks: TaskDef[], +) => { + return tasks.reduce( + (acc: TaskWithUnknownReference[], task): TaskWithUnknownReference[] => { + const parameters = findVariableReferencesInInputParameters( + variableTaskReferences, + task, + ); + if (_isEmpty(parameters)) { + return acc; + } + return acc.concat({ + task, + parameters, + expressions: [], + }); + }, + [], + ); +}; + +function findObjectWithValue(arr: string[], obj: any) { + const matchingKeys: RefractorObject[] = []; + for (const [key, value] of _entries(obj?.outputParameters)) { + if (arr.includes(key)) { + matchingKeys.push({ [key]: value, workflowName: obj?.name }); + } + } + return matchingKeys; +} + +const valueCrawler = (value: any, path = ""): string[] => { + if (typeof value === "string") { + return [path]; + } + if (_isArray(value)) { + return [path]; + } + + if (_isObject(value)) { + const objResult = Object.entries(value).flatMap( + ([key, value]) => valueCrawler(value, `${path}.${key}`), + 0, + ); + return _isEmpty(objResult) ? [path] : objResult; + } + return []; +}; + +export const buildInputParameterNotationTree = ( + inputParameters: Record, + path: string, +): string[] => { + return Object.entries(inputParameters).flatMap(([key, value]) => + valueCrawler(value, `${path}.${key}`), + ); +}; + +export const removedTasksToUnmatchedReferences = ({ + existingTaskReferences, + lastTaskRoute, +}: TaskReferenceReportingParameters): Array => { + return findUnMatchedTaskReferences(existingTaskReferences, lastTaskRoute); +}; + +type TaskReferenceTaskTuple = [string[], TaskDef[]]; + +export const workflowParameterToValidationError = ( + obj: RefractorObject, +): ValidationError => { + const firstKey = _first(Object.keys(obj)); + const reference = firstKey ? firstKey : ""; + const variableName = obj[reference] ? obj[reference] : ""; + const errorKind = variableName.includes("workflow") ? "workflow" : "task"; + const message = `'${firstKey ? firstKey : ""}' references unknown ${ + errorKind === "workflow" + ? `workflow variable - '${variableName}'` + : `task variable - '${variableName}'` + }`; + const legacyRefWarning = `'${ + firstKey ? firstKey : "" + }' references '${variableName}', is a legacy ref and should be replaced by 'task_ref_name.taskId'`; + return { + id: ErrorIds.REFERENCE_PROBLEMS, + message: variableName === `\${CPEWF_TASK_ID}` ? legacyRefWarning : message, + type: ErrorTypes.WORKFLOW, + severity: ErrorSeverity.WARNING, + }; +}; + +export const createJoinOnReferenceError = ( + task: TaskDef, + missingRef: string, +) => { + return { + id: ErrorIds.REFERENCE_PROBLEMS, + taskReferenceName: task.taskReferenceName, + message: `joinOn references missing taskReferenceName '${missingRef}'`, + type: ErrorTypes.TASK, + severity: ErrorSeverity.WARNING, + }; +}; + +export const taskReferenceProblemToTaskErrors = ( + tp: TaskWithUnknownReference, +): TaskErrors => { + const values = tp.parameters?.map((param) => { + const path = param + .split(".") + .reduce((obj: any, key) => obj?.[key], tp?.task?.inputParameters); + return path; + }); + const parameterErrors = (tp.parameters || []).map((p, i) => ({ + id: ErrorIds.REFERENCE_PROBLEMS, + taskReferenceName: tp.task.taskReferenceName, + message: + values[i] === `\${CPEWF_TASK_ID}` + ? `input parameter '${p}' references '\${CPEWF_TASK_ID}', A legacy ref and should be replaced by 'task_ref_name.taskId'` + : `input parameter '${p}' references non existing variable`, + type: ErrorTypes.TASK, + severity: ErrorSeverity.WARNING, + })); + const joinOnErrors = (tp.joinOn || []).map((missingRef) => + createJoinOnReferenceError(tp.task, missingRef), + ); + return { + task: tp.task, + errors: [...parameterErrors, ...joinOnErrors], + }; +}; + +export const expressionReferenceProblemToInputParametersErrors = ( + tp: TaskWithUnknownReference, +): TaskErrors => { + return { + task: tp.task, + errors: tp.expressions.map((p) => ({ + id: ErrorIds.REFERENCE_PROBLEMS, + taskReferenceName: tp.task.taskReferenceName, + message: + tp.task.type === TaskType.JDBC + ? `'statement' ${p}` + : `expression input parameter '${p}' does not exist`, + type: ErrorTypes.TASK, + severity: ErrorSeverity.WARNING, + })), + }; +}; + +export const testForRemovedTaskReferencesService = async ( + context: ErrorInspectorMachineContext, +): Promise => { + const { currentWf: workflow, crumbMap, secrets, envs } = context; + try { + const [existingTaskReferences, tasks] = Object.entries(crumbMap!).reduce( + ( + acc: TaskReferenceTaskTuple, + [taskReferenceName, { task }], + ): TaskReferenceTaskTuple => { + const taskReferences: string[] = acc[0].concat(taskReferenceName); + const cTask: TaskDef[] = acc[1].concat(task); + const rTuple: TaskReferenceTaskTuple = [taskReferences, cTask]; + return rTuple; + }, + [[], []], + ); + + const possibleWfParametesPath = (workflow?.inputParameters || []).map( + (p) => `workflow.input.${p}`, + ); + const envNames = Object.keys(envs || {}); + + const secretNames = (secrets || []).map( + (item: Record) => item?.name, + ); + const possibleSecretsNamePath = (secretNames || []).map( + (p) => `workflow.secrets.${p}`, + ); + + const possibleEnvPaths = (envNames || []).map((p) => `workflow.env.${p}`); + + const basicValidation = () => { + return existingTaskReferences + .concat("workflow.secrets") + .concat("workflow.env") + .concat("workflow.input") + .concat(DEFAULT_WF_ATTRIBUTES); + }; + const advancedValidation = () => { + return existingTaskReferences + .concat(possibleWfParametesPath) + .concat(possibleEnvPaths) + .concat(possibleSecretsNamePath) + .concat(DEFAULT_WF_ATTRIBUTES); + }; + + const possibleReferences = enabledAdvancedValidations + ? advancedValidation() + : basicValidation(); + + const unMatchedTasks = removedTasksToUnmatchedReferences({ + existingTaskReferences: possibleReferences, + lastTaskRoute: tasks, + }); + + const unMatchedTasksForVariable = findUnMatchedVariableReferencesTaks( + getVariablesForEachTasks(crumbMap!), + tasks, + ); + + const unMatchesInWorkflow = findUnMatchedWorkflowReferences( + possibleReferences, + workflow!, + ); + + const unMatchedReferenceTasks = [ + ...unMatchedTasks, + ...(enabledAdvancedValidations ? unMatchedTasksForVariable : []), + ]; + + const expressionInputRefProblems = unMatchedReferenceTasks.reduce( + (acc, task) => { + const mapped: TaskErrors = + expressionReferenceProblemToInputParametersErrors(task); + if (mapped.errors.length > 0) { + acc.push(mapped); + } + return acc; + }, + [] as TaskErrors[], + ); + + const taskRefProblems = unMatchedReferenceTasks.reduce((acc, task) => { + const mapped: TaskErrors = taskReferenceProblemToTaskErrors(task); + if (mapped.errors.length > 0) { + acc.push(mapped); + } + return acc; + }, [] as TaskErrors[]); + + const consolidatedTaskReferenceProblems = [ + ...taskRefProblems, + ...expressionInputRefProblems, + ]; + + return Promise.resolve({ + workflowReferenceProblems: unMatchesInWorkflow.map( + workflowParameterToValidationError, + ), + taskReferencesProblems: consolidatedTaskReferenceProblems, + unreachableTaskProblems: [], + }); + } catch { + return { + workflowReferenceProblems: [], + taskReferencesProblems: [], + unreachableTaskProblems: [], + }; + } +}; + +export const fetchSecrets = async ({ + authHeaders: headers, +}: ErrorInspectorMachineContext) => { + const url = `/secrets-v2`; + try { + const result = await queryClient.fetchQuery([fetchContext.stack, url], () => + fetchWithContext(url, fetchContext, { headers }), + ); + return result; + } catch (error) { + return Promise.reject(error); + } +}; + +export const fetchSecretsEndEnvironmentsList = async ( + context: ErrorInspectorMachineContext, +) => { + const secrets = enabledAdvancedValidations ? await fetchSecrets(context) : []; + const envs = enabledAdvancedValidations + ? await getEnvVariables({ authHeaders: context.authHeaders! }) + : []; + return { secrets, envs }; +}; + +export function isValidNestedVariable( + arrayOfStrings: string[], + valueString: string, + variables?: string[], +): boolean { + // regex to check valid nested variables + const regex = /\${(?:[a-zA-Z0-9_.\-\\[\]]|\$\{[a-zA-Z0-9_.\-\\[\]]+\})+}/g; + + return ( + (valueString.match(regex) !== null && + valueString.match(regex)?.every((match) => { + const variablesFromMatch = + _nth(match.substring(2, match.length - 1).split(".${"), 0) ?? ""; + + const innerValue = match.substring(2, match.length - 1); + if (innerValue.includes(".${")) { + return isValidNestedVariable(arrayOfStrings, innerValue, [ + variablesFromMatch, + ]); + } + const parts = [...(variables ?? []), variablesFromMatch]; + const [outermostParent, ...children] = [...parts]; + return ( + outermostParent === "workflow.secrets" && + children.every((item) => arrayOfStrings.includes(item)) + ); + })) ?? + false + ); +} diff --git a/ui-next/src/pages/definition/errorInspector/state/types.ts b/ui-next/src/pages/definition/errorInspector/state/types.ts new file mode 100644 index 0000000000..198e92210c --- /dev/null +++ b/ui-next/src/pages/definition/errorInspector/state/types.ts @@ -0,0 +1,286 @@ +import { NodeTaskData } from "components/flow/nodes/mapper"; +import { NodeData } from "reaflow"; +import { TaskDef, WorkflowDef, Crumb, CrumbMap, AuthHeaders } from "types"; +import { ImportSummary } from "utils/cloudTemplates"; + +export enum ErrorSeverity { + WARNING = "WARNING", + ERROR = "ERROR", +} + +export enum ErrorTypes { + WORKFLOW = "WORKFLOW", + TASK = "TASK", + SERVER_ERROR = "SERVER_ERROR", + RUN_ERROR = "RUN_ERROR", +} + +export enum ErrorIds { + TASK_TYPE_NOT_PRESENT = "task-type-not-present", + UNKNOWN_TASK_TYPE = "unknown-task-type", + TASK_REQUIRED_FIELD_MISSING = "task-required-field-missing", + TASK_REQUIRED_INPUT_PARAMETERS_MISSING = "task-required-input-parameters", + ALLOWED_VALUES = "non-allowed-values", + TYPE_ERROR = "type-error", + GENERIC_ERROR = "generic-error", + FLOW_ERROR = "flow-error", + REFERENCE_PROBLEMS = "reference-problems", + UNREACHABLE_TASK = "unreachable-task", + SERIALIZATION_ERROR = "serialization-error", +} + +export enum ErrorInspectorEventTypes { + VALIDATE_WORKFLOW_STRING = "VALIDATE_WORKFLOW_STRING", + VALIDATE_WORKFLOW = "VALIDATE_WORKFLOW", + VALIDATE_SINGLE_TASK = "VALIDATE_SINGLE_TASK", + SINGLE_TASK_ERRORS = "SINGLE_TASK_ERRORS", + REPORT_SERVER_ERROR = "REPORT_SERVER_ERROR", + REPORT_FLOW_ERROR = "REPORT_FLOW_ERROR", + REPORT_RUN_ERROR = "REPORT_RUN_ERROR", + WORKFLOW_WITH_NO_ERRORS = "WORKFLOW_WITH_NO_ERRORS", + WORKFLOW_HAS_ERRORS = "WORKFLOW_HAS_ERRORS", + + TOGGLE_TASK_ERRORS_VIEWER = "TOGGLE_TASK_ERRORS_VIEWER", + TOGGLE_WORKFLOW_ERRORS_VIEWER = "TOGGLE_WORKFLOW_ERRORS_VIEWER", + + CLICK_REFERENCE = "CLICK_REFERENCE", + + CLEAN_SERVER_ERRORS = "CLEAN_SERVER_ERRORS", + CLEAN_RUN_ERRORS = "CLEAN_RUN_ERRORS", + CLEAN_SERIALIZATION_ERROR = "CLEAN_SERIALIZATION_ERROR", + + REMOVED_TASK_REFERENCES = "REMOVED_TASK_REFERENCES", + FLOW_FINISHED_RENDERING = "FLOW_FINISHED_RENDERING", + + SET_WORKFLOW = "SET_WORKFLOW", + + UPDATE_SECRETS = "UPDATE_SECRETS", + + TOGGLE_TASK_REFERENCE_ERRORS_VIEWER = "TOGGLE_TASK_REFERENCE_ERRORS_VIEWER", + TOGGLE_WORKFLOW_REFERENCE_ERRORS_VIEWER = "TOGGLE_WORKFLOW_REFERENCE_ERRORS_VIEWER", + + TOGGLE_ERROR_INSPECTOR = "TOGGLE_ERROR_INSPECTOR", + SET_ERROR_INSPECTOR_EXPANDED = "SET_ERROR_INSPECTOR_EXPANDED", + SET_ERROR_INSPECTOR_COLLAPSED = "SET_ERROR_INSPECTOR_COLLAPSED", + + JUMP_TO_FIRST_ERROR = "JUMP_TO_FIRST_ERROR", + + COLLAPSE_INSPECTOR_IF_NO_ERRORS = "COLLAPSE_INSPECTOR_IF_NO_ERRORS", +} + +export type ServerValidationError = { + message?: string; + path?: string; + invalidValue?: string; +}; + +export type TaskHistory = { + taskPath: string; + task: TaskDef; +}; +export type StoredValidationError = ServerValidationError & + Partial; +export interface ValidationError { + id: ErrorIds; + message: string; + hint?: string; + taskReferenceName?: string; + path?: string; + type: ErrorTypes; + severity: ErrorSeverity; + onClickReference?: (data: string) => void; + taskError?: any; + validationErrors?: Array; +} + +export interface TaskErrors { + task: TaskDef; + errors: ValidationError[]; +} + +export interface SchemaValidationResponse { + taskErrors: TaskErrors[]; + workflowErrors: ValidationError[]; + tab?: number; +} + +export interface SchemaStringValidationResponse extends SchemaValidationResponse { + currentWf?: Partial; +} + +export interface TaskWithUnknownReference { + task: TaskDef; + parameters: string[]; + expressions: string[]; + joinOn?: string[]; +} + +export interface ReferenceProblems { + workflowReferenceProblems: ValidationError[]; + taskReferencesProblems: TaskErrors[]; + unreachableTaskProblems: TaskErrors[]; +} + +export interface ErrorInspectorMachineContext + extends SchemaValidationResponse, ReferenceProblems { + currentWf?: Partial; + serverErrors: ValidationError[]; + runWorkflowErrors: ValidationError[]; + crumbMap?: CrumbMap; + secrets?: Record[]; + envs?: Record; + authHeaders?: AuthHeaders; + expanded?: boolean; + importSummary?: ImportSummary; +} + +export type ValidateWorkflowStringEvent = { + type: ErrorInspectorEventTypes.VALIDATE_WORKFLOW_STRING; + workflowChanges: string; +}; + +export type ValidateWorkflowEvent = { + type: ErrorInspectorEventTypes.VALIDATE_WORKFLOW; + workflow: Partial; +}; + +export type UpdateSecretsEvent = { + type: ErrorInspectorEventTypes.UPDATE_SECRETS; + data?: { secrets: Record[]; envs: Record }; +}; + +export type WorkflowWithNoErrorsEvent = { + type: ErrorInspectorEventTypes.WORKFLOW_WITH_NO_ERRORS; + workflow: WorkflowDef; +}; + +export type WorkflowHasErrorsEvent = { + type: ErrorInspectorEventTypes.WORKFLOW_HAS_ERRORS; + errors: SchemaValidationResponse; + workflow: WorkflowDef; +}; + +export type FlowReportedErrorEvent = { + type: ErrorInspectorEventTypes.REPORT_FLOW_ERROR; + text: string; +}; + +export type ReportRunErrorEvent = { + type: ErrorInspectorEventTypes.REPORT_RUN_ERROR; + text: string; +}; + +export type ReportServerErrorEvent = { + type: ErrorInspectorEventTypes.REPORT_SERVER_ERROR; + text: string; + validationErrors?: ServerValidationError[]; +}; + +export type ValidateSingleTaskEvent = { + type: ErrorInspectorEventTypes.VALIDATE_SINGLE_TASK; + task: TaskDef; +}; + +export type FlowFinishedRenderingEvent = { + type: ErrorInspectorEventTypes.FLOW_FINISHED_RENDERING; + nodes: NodeData>[]; +}; + +export interface TaskReferenceReportingParameters { + existingTaskReferences: string[]; + lastTaskRoute: TaskDef[]; +} + +export type ReportTaskReferencesEvent = { + type: ErrorInspectorEventTypes.REMOVED_TASK_REFERENCES; + removedTask: TaskDef; + lastTaskCrumbs: Crumb[]; + workflow: Partial; +}; + +export type SetWorkflowEvent = { + type: ErrorInspectorEventTypes.SET_WORKFLOW; + workflow: Partial; +}; + +export type ToggleTaskErrorViewerEvent = { + type: ErrorInspectorEventTypes.TOGGLE_TASK_ERRORS_VIEWER; +}; + +export type ToggleWorkflowErrorViewerEvent = { + type: ErrorInspectorEventTypes.TOGGLE_WORKFLOW_ERRORS_VIEWER; +}; + +export type ToggleClickReference = { + type: ErrorInspectorEventTypes.CLICK_REFERENCE; + referenceText: string; +}; + +export type ToggleTaskReferenceErrorViewerEvent = { + type: ErrorInspectorEventTypes.TOGGLE_TASK_REFERENCE_ERRORS_VIEWER; +}; + +export type ToggleWorkflowReferenceErrorViewerEvent = { + type: ErrorInspectorEventTypes.TOGGLE_WORKFLOW_REFERENCE_ERRORS_VIEWER; +}; + +export type CleanServerErrorsEvent = { + type: ErrorInspectorEventTypes.CLEAN_SERVER_ERRORS; +}; + +export type CleanSerializationErrorEvent = { + type: ErrorInspectorEventTypes.CLEAN_SERIALIZATION_ERROR; +}; + +export type CleanRunErrorsEvent = { + type: ErrorInspectorEventTypes.CLEAN_RUN_ERRORS; +}; + +export type ToggleErrorInspectorEvent = { + type: ErrorInspectorEventTypes.TOGGLE_ERROR_INSPECTOR; +}; + +export type SetErrorInspectorExpandedEvent = { + type: ErrorInspectorEventTypes.SET_ERROR_INSPECTOR_EXPANDED; +}; + +export type SetErrorInspectorCollapsedEvent = { + type: ErrorInspectorEventTypes.SET_ERROR_INSPECTOR_COLLAPSED; +}; + +export interface RefractorObject { + [key: string]: string; +} + +export type JumpToFirstErrorEvent = { + type: ErrorInspectorEventTypes.JUMP_TO_FIRST_ERROR; +}; + +export type CollapseInspectorIfNoErrorsEvent = { + type: ErrorInspectorEventTypes.COLLAPSE_INSPECTOR_IF_NO_ERRORS; +}; + +export type ErrorInspectorMachineEvents = + | ReportTaskReferencesEvent + | ValidateWorkflowStringEvent + | FlowReportedErrorEvent + | ToggleTaskReferenceErrorViewerEvent + | ToggleWorkflowReferenceErrorViewerEvent + | FlowFinishedRenderingEvent + | CleanServerErrorsEvent + | ReportServerErrorEvent + | ToggleTaskErrorViewerEvent + | ToggleWorkflowErrorViewerEvent + | ValidateWorkflowEvent + | SetWorkflowEvent + | ValidateSingleTaskEvent + | UpdateSecretsEvent + | ToggleClickReference + | ToggleErrorInspectorEvent + | SetErrorInspectorExpandedEvent + | JumpToFirstErrorEvent + | SetErrorInspectorCollapsedEvent + | ReportRunErrorEvent + | CleanRunErrorsEvent + | CleanSerializationErrorEvent + | CollapseInspectorIfNoErrorsEvent; diff --git a/ui-next/src/pages/definition/helper.test.ts b/ui-next/src/pages/definition/helper.test.ts new file mode 100644 index 0000000000..f595d7bc15 --- /dev/null +++ b/ui-next/src/pages/definition/helper.test.ts @@ -0,0 +1,115 @@ +import { TaskDef } from "types/common"; +import { extractVariablesFromTask } from "./helpers"; + +const tasks = [ + { + name: "set_variable", + taskReferenceName: "set_variable_ref", + type: "SET_VARIABLE", + inputParameters: { + name: "Orkes", + }, + }, + { + name: "query_processor", + taskReferenceName: "query_processor_ref", + inputParameters: { + workflowNames: [], + statuses: ["FAILED"], + correlationIds: [], + queryType: "CONDUCTOR_API", + freeText: "automation test", + }, + type: "QUERY_PROCESSOR", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + expression: + "(function(){ \n const nameAndVersions = $.results.map(({workflowType,version})=>({name:workflowType,version})); \n return nameAndVersions;\n })();", + evaluatorType: "graaljs", + results: "${query_processor_ref.output.result.workflows}", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, +]; + +const taskWithoutVariables = [ + { + name: "query_processor", + taskReferenceName: "query_processor_ref", + inputParameters: { + workflowNames: [], + statuses: ["FAILED"], + correlationIds: [], + queryType: "CONDUCTOR_API", + freeText: "automation test", + }, + type: "QUERY_PROCESSOR", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + expression: + "(function(){ \n const nameAndVersions = $.results.map(({workflowType,version})=>({name:workflowType,version})); \n return nameAndVersions;\n })();", + evaluatorType: "graaljs", + results: "${query_processor_ref.output.result.workflows}", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, +]; + +describe("extractVariablesFromTask", () => { + it("Extract variables from tasks with set variable tasks", () => { + const result = extractVariablesFromTask(tasks as unknown as TaskDef[]); + expect(result).toEqual(["name"]); + }); + it("Extract variables from tasks without set variable tasks", () => { + const result = extractVariablesFromTask( + taskWithoutVariables as unknown as TaskDef[], + ); + expect(result).toEqual([]); + }); +}); diff --git a/ui-next/src/pages/definition/helpers.ts b/ui-next/src/pages/definition/helpers.ts new file mode 100644 index 0000000000..a82f4907a3 --- /dev/null +++ b/ui-next/src/pages/definition/helpers.ts @@ -0,0 +1,61 @@ +import _pick from "lodash/pick"; +import { InlineTaskInputParameters } from "types/TaskType"; +import { WorkflowDef, WorkflowMetadataI } from "types/WorkflowDef"; +import _keys from "lodash/keys"; +import _difference from "lodash/difference"; +import { TaskDef, TaskType } from "types/common"; + +export const extractWorkflowMetadata = (workflow: Partial) => + _pick(workflow, [ + "name", + "description", + "version", + "restartable", + "workflowStatusListenerEnabled", + "timeoutSeconds", + "timeoutPolicy", + "failureWorkflow", + "ownerEmail", + "updateTime", + "inputParameters", + "outputParameters", + "inputSchema", + "outputSchema", + "enforceSchema", + "tasks", + "workflowStatusListenerSink", + "rateLimitConfig", + "metadata", + ]) as Partial; + +export const undeclaredInputParameters = ( + inputString: string, + taskInputParams?: InlineTaskInputParameters | Record, +) => { + const matchedVariables = inputString.match(/(?<=\$\.)\w+/g); + const inputParameters = _keys(taskInputParams); + let addedInputParameters: string[] = []; + if (matchedVariables) { + addedInputParameters = _difference(matchedVariables, inputParameters); + } + return addedInputParameters; +}; + +export const invalidDollarVariables = (inputString: string) => { + const regex = /\$(?:\.\$)*\.[\w$]+(?=[\s;\n])/g; + const matches = inputString.match(regex); + const filteredMatches = matches?.filter( + (match: any) => (match.match(/\$/g) || []).length > 1, + ); + return filteredMatches ?? []; +}; + +export const extractVariablesFromTask = (tasksInCrumbBranch: TaskDef[]) => { + const setVariableInputs = tasksInCrumbBranch.reduce((acc, task) => { + if (task?.type === TaskType.SET_VARIABLE) { + Object.assign(acc, task?.inputParameters || {}); + } + return acc; + }, {}); + return Object.keys(setVariableInputs); +}; diff --git a/ui-next/src/pages/definition/progressicons.jsx b/ui-next/src/pages/definition/progressicons.jsx new file mode 100644 index 0000000000..9b262f068d --- /dev/null +++ b/ui-next/src/pages/definition/progressicons.jsx @@ -0,0 +1,21 @@ +const style = { + stroke: "#0971f1", + strokeDasharray: 93, + strokeDashoffset: 93, + strokeWidth: 2, + fill: "transparent", +}; +export default function ProgressIcon() { + return ( + + + + + + ); +} diff --git a/ui-next/src/pages/definition/state/WorkflowEditContext/WorkflowEditContext.tsx b/ui-next/src/pages/definition/state/WorkflowEditContext/WorkflowEditContext.tsx new file mode 100644 index 0000000000..8c01e1d396 --- /dev/null +++ b/ui-next/src/pages/definition/state/WorkflowEditContext/WorkflowEditContext.tsx @@ -0,0 +1,6 @@ +import { createContext } from "react"; +import { WorkflowEditContextProps } from "./types"; + +export const WorkflowEditContext = createContext({ + workflowDefinitionActor: undefined, +}); diff --git a/ui-next/src/pages/definition/state/WorkflowEditContext/WorkflowEditProvider.tsx b/ui-next/src/pages/definition/state/WorkflowEditContext/WorkflowEditProvider.tsx new file mode 100644 index 0000000000..8abac762a7 --- /dev/null +++ b/ui-next/src/pages/definition/state/WorkflowEditContext/WorkflowEditProvider.tsx @@ -0,0 +1,21 @@ +import { FunctionComponent, ReactNode } from "react"; +import { ActorRef } from "xstate"; +import { WorkflowDefinitionEvents } from "../types"; +import { WorkflowEditContext } from "./WorkflowEditContext"; + +interface WorkflowEditContextProps { + workflowDefinitionActor?: ActorRef; + children?: ReactNode; +} + +export const FlowEditContextProvider: FunctionComponent< + WorkflowEditContextProps +> = ({ workflowDefinitionActor, children }) => ( + + {children} + +); diff --git a/ui-next/src/pages/definition/state/WorkflowEditContext/index.ts b/ui-next/src/pages/definition/state/WorkflowEditContext/index.ts new file mode 100644 index 0000000000..d7d6c0a95b --- /dev/null +++ b/ui-next/src/pages/definition/state/WorkflowEditContext/index.ts @@ -0,0 +1,2 @@ +export * from "./WorkflowEditContext"; +export * from "./WorkflowEditProvider"; diff --git a/ui-next/src/pages/definition/state/WorkflowEditContext/types.ts b/ui-next/src/pages/definition/state/WorkflowEditContext/types.ts new file mode 100644 index 0000000000..20dfe455a0 --- /dev/null +++ b/ui-next/src/pages/definition/state/WorkflowEditContext/types.ts @@ -0,0 +1,8 @@ +import { ReactNode } from "react"; +import { ActorRef } from "xstate"; +import { WorkflowDefinitionEvents } from "../types"; + +export interface WorkflowEditContextProps { + workflowDefinitionActor?: ActorRef; + children?: ReactNode; +} diff --git a/ui-next/src/pages/definition/state/action.ts b/ui-next/src/pages/definition/state/action.ts new file mode 100644 index 0000000000..5f4ab2e0d5 --- /dev/null +++ b/ui-next/src/pages/definition/state/action.ts @@ -0,0 +1,1043 @@ +import { FlowActionTypes, FlowEvents } from "components/flow/state"; +import _first from "lodash/first"; +import _has from "lodash/has"; +import _isNil from "lodash/isNil"; +import _last from "lodash/last"; +import { newWorkflowTemplate } from "templates/JSONSchemaWorkflow"; +import { WorkflowDef } from "types/WorkflowDef"; +import { + adjust, + defaultValueFromSchema, + flattenGtagObject, + getSequentiallySuffix, + gtagAbstract, + logger, +} from "utils"; +import { ActorRef, assign, DoneInvokeEvent, forwardTo } from "xstate"; +import { choose, log, pure, raise, sendTo } from "xstate/lib/actions"; +import { + ErrorInspectorEventTypes, + ErrorInspectorMachineEvents, + WorkflowWithNoErrorsEvent, +} from "../errorInspector/state"; +import { CODE_TAB, RUN_TAB, SEVERITY_ERROR, TASK_TAB } from "./constants"; +import { + ADD_NEW_SWITCH_PATH, + performOperation as applyWorkflowOperation, + DELETE_TASK, + moveTask, + positionIdentifier, + REMOVE_BRANCH, + REPLACE_TASK, +} from "./taskModifier"; +import { + AddNewSwitchTaskEvent, + ChangeTabEvent, + ChangeVersionEvent, + DefinitionMachineContext, + DefinitionMachineEventTypes, + DeleteRequestEvent, + DONT_SHOW_IMPORT_SUCCESSFUL_DIALOG_TUTORIAL_AGAIN, + HandleLeftPanelExpandedEvent, + HandleSaveAndCreateNewEvent, + HandleSaveAndRunEvent, + LeftPaneTabs, + MoveTaskEvent, + PerformOperationEvent, + RemoveBranchFromTaskEvent, + RemoveTaskEvent, + ReplaceTaskEvent, + ResetRequestEvent, + SaveAndCreateNewRequestEvent, + SaveAndRunRequestEvent, + SaveAsNewVersionRequestEvent, + SyncRunContextAndChangeTabEvent, + ToggleAgentExpandedEvent, + UpdateAttributesEvent, + UpdateWorkflowMetadataEvent, +} from "./types"; + +import { JsonSchema } from "@jsonforms/core"; +import { crumbsToTask } from "components/flow/nodes"; +import _isEmpty from "lodash/isEmpty"; +import { CommonTaskDef } from "types/TaskType"; +import { ImportSummary } from "utils/cloudTemplates"; +import { SWITCH_CASE_PREFIX } from "utils/constants/switch"; +import { + LocalCopyMachineEventTypes, + UseLocalCopyChangesEvent, +} from "../ConfirmLocalCopyDialog/state"; +import { SavedSuccessfulEvent } from "../confirmSave/state"; +import { HighlightTextReferenceEvent } from "../EditorPanel/CodeEditorTab/state"; +import { + FormMachineActionTypes, + TaskFormEvents, +} from "../EditorPanel/TaskFormTab/state"; +import { RunMachineEvents, RunMachineEventsTypes } from "../RunWorkflow/state"; +import { + WorkflowMetadataEvents, + WorkflowMetadataMachineEventTypes, +} from "../WorkflowMetadata/state"; +export const persistWorkflowAttribs = assign< + DefinitionMachineContext, + UpdateAttributesEvent +>( + ( + context: DefinitionMachineContext, + { + isNewWorkflow, + workflowName, + currentVersion, + workflowTemplateId, + }: UpdateAttributesEvent, + ) => { + const newWorkflowDefinition: Partial = newWorkflowTemplate( + context.currentUserInfo?.id || "example@email.com", + ) as unknown as Partial; + const currentWf = isNewWorkflow ? newWorkflowDefinition : {}; + + return { + isNewWorkflow, + workflowName, + currentWf, + workflowChanges: currentWf, + currentVersion, + workflowTemplateId, + // Keep agent collapsed by default to improve initial page load performance + isAgentExpanded: context.isAgentExpanded ?? false, + }; + }, +); + +export const updateWf = assign< + DefinitionMachineContext, + DoneInvokeEvent<{ workflow: WorkflowDef }> +>(({ workflowChanges }, { data }) => { + return { + currentWf: data?.workflow, + // Because of reading workflow from local storage 1st + workflowChanges: _isEmpty(workflowChanges) + ? data?.workflow + : workflowChanges, + }; +}); + +export const updateWfDefaultRunParam = assign< + DefinitionMachineContext, + DoneInvokeEvent<{ schema: JsonSchema }> +>((_context, { data }) => { + const sanitizedDefaults = defaultValueFromSchema(data?.schema); + return { + workflowDefaultRunParam: sanitizedDefaults, + }; +}); + +export const updateSecretsAndEnvs = assign< + DefinitionMachineContext, + DoneInvokeEvent<{ + secrets: Record[]; + envs: Record; + }> +>((_, { data }) => { + return { + secrets: data.secrets, + envs: data.envs, + }; +}); + +export const resetChanges = assign({ + workflowChanges: ({ currentWf }, __) => currentWf, +}); + +export const updateCollapseWorkflowList = assign( + (context, event) => { + return { + collapseWorkflowList: event?.collapseWorkflowList, + }; + }, +); + +export const setVersion = assign({ + currentVersion: (_currentVersion, { version }) => version, +}); + +export const resetCurrentVersion = assign((_ctx, _event) => { + return { + currentVersion: null, + }; +}); + +export const setMessage = assign({ + message: (_ctx, messageObj) => messageObj, +}); + +export const processErrorFetching = assign< + DefinitionMachineContext, + DoneInvokeEvent<{ message: string }> +>((__, { data }) => ({ + message: { + text: data.message, + severity: SEVERITY_ERROR, + }, +})); + +export const resetMessage = assign((_ctx, __evt) => { + return { + message: { + text: undefined, + severity: undefined, + }, + }; +}); + +export const notifyFlowUpdates = sendTo< + DefinitionMachineContext, + any, + ActorRef +>("flowMachine", (ctx) => { + return { + type: FlowActionTypes.UPDATE_WF_DEFINITION_EVT, + workflow: ctx.workflowChanges, + cleanNodeSelection: ctx.selectedTaskCrumbs.length === 0, + }; +}); + +// Special version for agent updates that reads workflow directly from event +// instead of context, to avoid XState assign timing issues +export const notifyFlowUpdatesFromEvent = sendTo< + DefinitionMachineContext, + any, + ActorRef +>("flowMachine", (ctx, event) => { + return { + type: FlowActionTypes.UPDATE_WF_DEFINITION_EVT, + workflow: event.workflow, + cleanNodeSelection: ctx.selectedTaskCrumbs.length === 0, + }; +}); + +export const forwardCollapseWorkflowList = sendTo< + DefinitionMachineContext, + any, + ActorRef +>("flowMachine", (ctx, event) => { + return { + type: FlowActionTypes.UPDATE_COLLAPSE_WORKFLOW_LIST, + workflowName: event.workflowName, + }; +}); + +export const notifyFlowResetZoomPosition = sendTo< + DefinitionMachineContext, + any, + ActorRef +>("flowMachine", (_ctx) => { + return { + type: FlowActionTypes.RESET_ZOOM_POSITION, + }; +}); + +export const setFlowAsReadOnly = sendTo("flowMachine", (_ctx) => { + return { + type: FlowActionTypes.SET_READ_ONLY_EVT, + }; +}); + +export const changeTab = assign< + DefinitionMachineContext, + ChangeTabEvent | SyncRunContextAndChangeTabEvent +>({ + openedTab: (_context, event) => + "data" in event ? event.data.originalEvent.tab : event.tab, + previousTab: ({ openedTab }, _) => openedTab, +}); + +export const changeToCodeTab = assign({ + openedTab: CODE_TAB, + previousTab: ({ openedTab }: DefinitionMachineContext, _event) => openedTab, +}); + +export const changeToTaskTab = assign({ + openedTab: TASK_TAB, + previousTab: ({ openedTab }: DefinitionMachineContext, _event) => openedTab, +}); + +export const changeToPreviousTab = assign({ + openedTab: ({ previousTab, openedTab }: DefinitionMachineContext) => + previousTab === openedTab ? LeftPaneTabs.WORKFLOW_TAB : previousTab, +}); + +export const performOperation = assign< + DefinitionMachineContext, + PerformOperationEvent +>({ + workflowChanges: (context, { data, operation }) => { + const workflowAfterChanges = applyWorkflowOperation({ + workflow: context.workflowChanges, + crumbs: data.crumbs, + taskDef: data.task, + operation: { + type: data.action, + ...operation, + }, + }); + return workflowAfterChanges; + }, +}); + +export const replaceTask = assign({ + workflowChanges: (context, { task, crumbs, newTask }) => { + const workflowAfterChanges = applyWorkflowOperation({ + workflow: context?.workflowChanges, + crumbs, + taskDef: task, + operation: { + type: REPLACE_TASK, + payload: newTask, + }, + }); + return workflowAfterChanges; + }, +}); + +// export const cancelDebounceEditChanges = cancel("debounce_edit_event"); + +export const removeTask = assign({ + workflowChanges: (context, { task, crumbs }) => { + const workflowAfterChanges = applyWorkflowOperation({ + workflow: context?.workflowChanges, + crumbs, + taskDef: task, + operation: { + type: DELETE_TASK, + payload: {}, + }, + }); + return workflowAfterChanges; + }, +}); + +export const addNewSwitchStatementToTask = assign< + DefinitionMachineContext, + AddNewSwitchTaskEvent +>({ + workflowChanges: (context, { task, crumbs }) => { + const currentPathNames = task.decisionCases + ? Object.keys(task.decisionCases) + : []; + + const workflowAfterChanges = applyWorkflowOperation({ + workflow: context?.workflowChanges, + crumbs, + taskDef: task, + operation: { + type: ADD_NEW_SWITCH_PATH, + payload: { + branchName: getSequentiallySuffix({ + name: SWITCH_CASE_PREFIX, + refNames: currentPathNames, + }).name, + }, + }, + }); + return workflowAfterChanges; + }, +}); + +export const removeBranchFromTask = assign< + DefinitionMachineContext, + RemoveBranchFromTaskEvent +>({ + workflowChanges: (context) => { + const { workflowChanges, lastRemovalOperation } = context; + const { crumbs, task, branchName } = lastRemovalOperation!; + const workflowAfterChanges = applyWorkflowOperation({ + workflow: workflowChanges, + crumbs, + taskDef: task, + operation: { + type: REMOVE_BRANCH, + payload: { + branchName, + }, + }, + }); + return workflowAfterChanges; + }, +}); + +export const updateWFMetadata = assign< + DefinitionMachineContext, + UpdateWorkflowMetadataEvent +>({ + workflowChanges: (context, { workflowMetadata }) => { + const updatedWf: Partial = { + ...context.workflowChanges, + ...workflowMetadata, + }; + return updatedWf; + }, +}); + +export const forwardToCodeMachine = forwardTo("codeMachine"); + +export const forwardToSaveMachine = forwardTo("saveChangesMachine"); + +export const selectNewTask = sendTo< + DefinitionMachineContext, + any, + ActorRef +>("flowMachine", ({ lastPerformedOperation: operation }, _) => { + return { + type: FlowActionTypes.SELECT_NODE_EVT, + node: { + id: (Array.isArray(operation?.payload) + ? (_first(operation?.payload) as CommonTaskDef | undefined) + ?.taskReferenceName + : operation?.payload?.taskReferenceName)!, + }, + }; +}); + +export const cleanLastOperation = assign({ + lastPerformedOperation: undefined, +}); + +export const cleanTaskCrumbSelection = assign({ + selectedTaskCrumbs: [], +}); + +export const updateSelectedCrumbs = assign< + DefinitionMachineContext, + ReplaceTaskEvent +>({ + selectedTaskCrumbs: ({ selectedTaskCrumbs }, { newTask }) => { + return adjust( + selectedTaskCrumbs.length - 1, + () => ({ + ..._last(selectedTaskCrumbs), + ref: newTask.taskReferenceName, + }), + selectedTaskCrumbs, + ); + }, +}); + +export const persistLastOperation = assign< + DefinitionMachineContext, + PerformOperationEvent +>({ + lastPerformedOperation: (context, event) => { + return event.operation; + }, +}); + +export const validateWorkflow = sendTo< + DefinitionMachineContext, + any, + ActorRef +>("errorInspectorMachine", ({ workflowChanges }) => { + return { + type: ErrorInspectorEventTypes.VALIDATE_WORKFLOW, + workflow: workflowChanges || {}, + }; +}); + +export const forwardCleanWorkflow = sendTo< + DefinitionMachineContext, + WorkflowWithNoErrorsEvent, + ActorRef +>("flowMachine", (_ctx, { workflow }) => { + return { + type: FlowActionTypes.UPDATE_WF_DEFINITION_EVT, + workflow, + cleanNodeSelection: false, + }; +}); + +export const sendCrumbUpdates = sendTo< + DefinitionMachineContext, + any, + ActorRef +>("formTaskMachine", (ctx) => { + return { + type: FormMachineActionTypes.UPDATE_CRUMBS, + crumbs: ctx.selectedTaskCrumbs, + task: crumbsToTask( + ctx.selectedTaskCrumbs, + ctx.workflowChanges?.tasks || [], + ), + }; +}); + +export const persistSelectedTabCrumbs = assign( + (_, event) => { + return { + selectedTaskCrumbs: event?.node?.data?.crumbs, + }; + }, +); + +export const forwardToErrorInspector = forwardTo("errorInspectorMachine"); + +export const forwardSelectEdge = forwardTo("formTaskMachine"); + +export const logStuff = (context: DefinitionMachineContext, event: any) => { + logger.error("[Error]: This is the context", context); + logger.error("[Error]: This is the event", event); +}; + +export const startRenderingGtag = ( + context: DefinitionMachineContext, + event: any, +) => { + const flattenEvent = flattenGtagObject(event, `event`); + const prefix = `event_at_workflow_${context?.workflowName}_start_rendering_diagram_request`; + gtagAbstract(prefix, { + user_uuid: context.currentUserInfo?.uuid, + workflow_name: context?.workflowName, + user_performed_action: event?.type, + start_time: new Date().getTime(), + ...flattenEvent, + }); +}; + +export const gtagEventLogger = ( + context: DefinitionMachineContext, + event: any, +) => { + const flattenEvent = flattenGtagObject(event, `event`); + const prefix = `event_at_workflow_${context?.workflowName}_of_type_${event?.type}`; + gtagAbstract(prefix, { + user_uuid: context.currentUserInfo?.uuid, + workflow_name: context?.workflowName, + user_performed_action: event?.type, + ...flattenEvent, + }); +}; +export const gtagErrorLogger = ( + context: DefinitionMachineContext, + event: any, +) => { + const flattenEvent = flattenGtagObject(event, "event"); + gtagAbstract(`error_${context?.workflowName}_${event?.type}`, { + user_uuid: context.currentUserInfo?.uuid, + workflow_name: context?.workflowName, + user_performed_action: event?.type, + ...flattenEvent, + }); +}; + +export const cleanServerErrors = sendTo( + "errorInspectorMachine", + (__context, _event) => { + return { + type: ErrorInspectorEventTypes.CLEAN_SERVER_ERRORS, + }; + }, +); + +export const cleanRunErrors = sendTo( + "errorInspectorMachine", + (__context, _event) => { + return { + type: ErrorInspectorEventTypes.CLEAN_RUN_ERRORS, + }; + }, +); + +export const persistWorkflowChanges = assign( + ({ workflowChanges }, { workflow }) => { + if (_isNil(workflow)) { + logger.info( + "persistWorkflowChanges: incoming workflow is null so staying with context changes.", + ); + + return { + workflowChanges, + }; + } + + return { + workflowChanges: workflow, + }; + }, +); + +export const sendWorkflowToInspector = sendTo< + DefinitionMachineContext, + any, + ActorRef +>("errorInspectorMachine", ({ workflowChanges }) => { + return { + type: ErrorInspectorEventTypes.SET_WORKFLOW, + workflow: workflowChanges || {}, + }; +}); + +export const sendWorkflowChangesToMetadataMachine = sendTo< + DefinitionMachineContext, + any, + ActorRef +>("workflowMetadataEditorMachine", ({ workflowChanges }) => { + return { + type: WorkflowMetadataMachineEventTypes.FORCE_WORKFLOW, + workflow: workflowChanges || {}, + }; +}); + +export const notifyToFlowIfOutputParameters = choose< + DefinitionMachineContext, + UpdateWorkflowMetadataEvent +>([ + { + cond: (_context, event) => + _has(event, "workflowMetadata.outputParameters") || + _has(event, "workflowMetadata.inputParameters"), + actions: ["notifyFlowUpdates"], + }, + { + actions: [ + (__c) => + log("Nothing to notify. outputParameters have not been modified"), + ], + }, +]); + +export const persistRemovalOperation = assign< + DefinitionMachineContext, + RemoveBranchFromTaskEvent +>({ + lastRemovalOperation: (__context, { type: _type, ...rest }) => rest, +}); + +export const cleanLastRemovalOperation = assign({ + lastRemovalOperation: undefined, +}); + +export const applyLastRemovalOperationAsRemoveTaskOperation = assign< + DefinitionMachineContext, + any +>({ + workflowChanges: (context) => { + const { lastRemovalOperation } = context; + const { crumbs, task } = lastRemovalOperation!; + const workflowAfterChanges = applyWorkflowOperation({ + workflow: context?.workflowChanges, + crumbs, + taskDef: task, + operation: { + type: DELETE_TASK, + payload: {}, + }, + }); + return workflowAfterChanges; + }, +}); + +export const forwardWorkflowToLocalCopyMachine = forwardTo("localCopyMachine"); + +export const forwardWorkflowToMetadataEditorMachine = forwardTo( + "workflowMetadataEditorMachine", +); + +export const forwardWorkflowToTabMetadataEditorMachine = forwardTo( + "workflowTabMetaEditor", +); + +export const removeLocalCopy = raise({ + type: LocalCopyMachineEventTypes.REMOVE_LOCAL_COPY, +}); + +export const persistWorkflowNameAndVersion = assign< + DefinitionMachineContext, + SavedSuccessfulEvent +>({ + workflowName: ({ workflowName }, { workflowName: eventWfName }) => + eventWfName ? eventWfName : workflowName, + currentVersion: ( + { currentVersion }, + { currentVersion: eventCurrentVersion }, + ) => (eventCurrentVersion ? `${eventCurrentVersion}` : currentVersion), + currentWf: (_context, { workflow }) => workflow, + workflowChanges: (_context, { workflow }) => workflow, + isNewWorkflow: (_context, { isNewWorkflow }) => isNewWorkflow, +}); + +export const maybePersistLocalCopyMessage = assign< + DefinitionMachineContext, + DoneInvokeEvent<{ + workflow: Partial; + isLocalStorageEmpty?: boolean; + }> +>({ + localCopyMessage: ({ isNewWorkflow }, event) => { + return isNewWorkflow && !event.data?.isLocalStorageEmpty + ? `Showing last unsaved workflow. ` + : undefined; + }, +}); + +export const moveTaskFromLocation = assign< + DefinitionMachineContext, + MoveTaskEvent +>({ + workflowChanges: (context, event) => { + const { + sourceTask, + sourceTaskCrumbs, + targetLocationCrumbs, + targetTask, + position, + } = event; + + return moveTask({ + workflow: context?.workflowChanges, + source: { task: sourceTask, crumbs: sourceTaskCrumbs }, + target: { crumbs: targetLocationCrumbs, task: targetTask }, + position: positionIdentifier(position), + }); + }, +}); + +export const selectMovedTask = sendTo< + DefinitionMachineContext, + any, + ActorRef +>( + "flowMachine", + (__context, { sourceTask }) => { + return { + type: FlowActionTypes.SELECT_NODE_EVT, + node: { + id: sourceTask?.taskReferenceName, + }, + }; + }, + { delay: 100 }, +); + +export const reSelectTaskIfSelected = pure( + ({ selectedTaskCrumbs }) => { + const lastCrumb = _last(selectedTaskCrumbs); + if (lastCrumb?.ref) { + return sendTo>( + "flowMachine", + (__context) => { + return { + type: FlowActionTypes.SELECT_NODE_EVT, + node: { + id: lastCrumb?.ref, + }, + }; + }, + { delay: 100 }, + ); + } + }, +); + +export const cleanLocalCopyMessage = assign({ + localCopyMessage: undefined, +}); + +export const updateWfFromLocalStorage = assign< + DefinitionMachineContext, + DoneInvokeEvent< + { workflow?: Partial } | UseLocalCopyChangesEvent + > +>((context, { data }) => { + return { + workflowChanges: data.workflow, + }; +}); + +export const fireChangeToWorkflowTab = raise({ + type: DefinitionMachineEventTypes.CHANGE_TAB_EVT, + tab: LeftPaneTabs.WORKFLOW_TAB, +}); + +export const fireChangeToCodeTab = raise({ + type: DefinitionMachineEventTypes.CHANGE_TAB_EVT, + tab: LeftPaneTabs.CODE_TAB, +}); + +export const fireChangeToRunTab = raise({ + type: DefinitionMachineEventTypes.CHANGE_TAB_EVT, + tab: LeftPaneTabs.RUN_TAB, +}); + +export const fireChangeToDependenciesTab = raise( + { + type: DefinitionMachineEventTypes.CHANGE_TAB_EVT, + tab: LeftPaneTabs.DEPENDENCIES_TAB, + }, +); + +export const handleLeftPanelExpanded = raise< + DefinitionMachineContext, + HandleLeftPanelExpandedEvent +>({ + type: DefinitionMachineEventTypes.HANDLE_LEFT_PANEL_EXPANDED, + onSelectNode: true, +}); + +export const persistCodeReference = assign< + DefinitionMachineContext, + HighlightTextReferenceEvent +>({ + codeTextReference: (_context, { reference }) => reference, +}); + +export const cleanCodeTextReference = assign({ + codeTextReference: undefined, +}); + +export const setRunTabAsPreviousTab = assign({ + previousTab: (_context, _event) => RUN_TAB, +}); + +export const fireSaveEvent = raise< + DefinitionMachineContext, + SaveAndRunRequestEvent +>({ + type: DefinitionMachineEventTypes.SAVE_EVT, + isSaveAndRun: true, +}); + +export const fireSaveAndCreateNewRequestEvent = raise< + DefinitionMachineContext, + SaveAndCreateNewRequestEvent +>((__, event) => ({ + type: DefinitionMachineEventTypes.SAVE_EVT, + originalEvent: event.type, +})); + +export const raiseResetEvent = raise< + DefinitionMachineContext, + ResetRequestEvent +>( + { + type: DefinitionMachineEventTypes.RESET_EVT, + }, + { delay: 200 }, +); +export const raiseDeleteEvent = raise< + DefinitionMachineContext, + DeleteRequestEvent +>( + { + type: DefinitionMachineEventTypes.DELETE_EVT, + }, + { delay: 200 }, +); +export const raiseSaveEvent = raise< + DefinitionMachineContext, + SaveAsNewVersionRequestEvent +>( + (__, { isNewVersion }) => ({ + type: DefinitionMachineEventTypes.SAVE_EVT, + isNewVersion, + }), + { delay: 200 }, +); + +export const raiseSaveAndRunEvent = raise< + DefinitionMachineContext, + HandleSaveAndRunEvent +>( + { + type: DefinitionMachineEventTypes.HANDLE_SAVE_AND_RUN, + }, + { delay: 200 }, +); + +export const justExecute = sendTo< + DefinitionMachineContext, + any, + ActorRef +>( + "runWorkflowMachine", + () => { + return { + type: RunMachineEventsTypes.TRIGGER_RUN_WORKFLOW, + }; + }, + { delay: 200 }, +); + +export const raiseSaveAndCreateNewEvent = raise< + DefinitionMachineContext, + HandleSaveAndCreateNewEvent +>( + { + type: DefinitionMachineEventTypes.HANDLE_SAVE_AND_CREATE_NEW, + }, + { delay: 200 }, +); + +export const maybeSelectInitialSelectedTaskReference = pure< + DefinitionMachineContext, + any +>(({ initialSelectedTaskReferenceName }) => { + if (initialSelectedTaskReferenceName != null) { + return sendTo>( + "flowMachine", + (__context) => { + return { + type: FlowActionTypes.SELECT_NODE_EVT, + node: { + id: initialSelectedTaskReferenceName, + }, + }; + }, + { delay: 50 }, + ); + } +}); + +export const cleanInitialSelectedTaskReferenceName = assign({ + initialSelectedTaskReferenceName: undefined, +}); + +export const setSaveSourceEventType = assign< + DefinitionMachineContext, + HandleSaveAndCreateNewEvent +>({ + saveSourceEventType: (_context, event) => event.type, +}); + +export const raiseUpdateAtribsEvent = raise< + DefinitionMachineContext, + UpdateAttributesEvent +>( + ( + context: DefinitionMachineContext, + { + isNewWorkflow, + workflowName, + workflowVersions, + currentVersion, + workflowTemplateId, + }: UpdateAttributesEvent, + ) => { + // Check if this is a "save and create new" operation by looking at the save source event type + const isContinueCreate = + context.saveSourceEventType === + DefinitionMachineEventTypes.HANDLE_SAVE_AND_CREATE_NEW; + + // If this is a "save and create new" operation, use new workflow attributes + if (isContinueCreate) { + return { + type: DefinitionMachineEventTypes.UPDATE_ATTRIBS_EVT, + workflowName: "NEW", // This will be replaced by persistWorkflowAttribs + isNewWorkflow: true, + workflowVersions: [], + currentVersion: undefined, + workflowTemplateId: undefined, + }; + } + + return { + type: DefinitionMachineEventTypes.UPDATE_ATTRIBS_EVT, + workflowName, + isNewWorkflow, + workflowVersions, + currentVersion, + workflowTemplateId, + }; + }, +); +export const persistWorkflowVersionsParsed = assign< + DefinitionMachineContext, + DoneInvokeEvent<{ versions: number[] }> +>((_context, { data: { versions } }) => { + return { + workflowVersions: versions, + }; +}); + +export const persistLatestVersion = assign< + DefinitionMachineContext, + DoneInvokeEvent<{ versions: string[] }> +>((_context, { data: { versions } }) => { + const latestVersion = _last(versions); + return { + currentVersion: latestVersion, + }; +}); + +export const markDontShowImportSuccessfulDialogAgain = () => { + localStorage.setItem( + DONT_SHOW_IMPORT_SUCCESSFUL_DIALOG_TUTORIAL_AGAIN, + "true", + ); +}; +export const showTaskDescriptions = sendTo< + DefinitionMachineContext, + any, + ActorRef +>( + "flowMachine", + (__context) => { + return { + type: FlowActionTypes.TOGGLE_SHOW_DESCRIPTION, + }; + }, + { delay: 100 }, +); + +export const persistImportSummary = assign< + DefinitionMachineContext, + DoneInvokeEvent +>({ + importSummary: (_context, { data }) => data, +}); + +export const reportErrorToErrorInspector = sendTo( + ({ errorInspectorMachine }) => errorInspectorMachine, + (_context, { data }: DoneInvokeEvent<{ message: string }>) => ({ + type: ErrorInspectorEventTypes.REPORT_SERVER_ERROR, + text: data.message, + }), +); + +export const cleanSerializationError = sendTo( + "errorInspectorMachine", + (_context, _event) => { + return { + type: ErrorInspectorEventTypes.CLEAN_SERIALIZATION_ERROR, + }; + }, +); + +export const updateRunTabFormState = assign< + DefinitionMachineContext, + SyncRunContextAndChangeTabEvent +>({ + runTabFormState: (_ctx, event) => event.data.runMachineContext, +}); + +export const toggleAgentExpanded = assign< + DefinitionMachineContext, + ToggleAgentExpandedEvent +>({ + isAgentExpanded: (context, event) => { + if (event.expanded !== undefined) { + return event.expanded; + } + return !context.isAgentExpanded; + }, +}); + +export const autoExpandAgentForNewWorkflow = assign< + DefinitionMachineContext, + any +>({ + isAgentExpanded: (context) => { + // Auto-expand agent for new workflows after diagram loads + return context.isNewWorkflow ? true : context.isAgentExpanded; + }, +}); + +export const forwardToRunWorkflowMachine = forwardTo("runWorkflowMachine"); diff --git a/ui-next/src/pages/definition/state/constants.js b/ui-next/src/pages/definition/state/constants.js new file mode 100644 index 0000000000..05623c6f3c --- /dev/null +++ b/ui-next/src/pages/definition/state/constants.js @@ -0,0 +1,40 @@ +export const themes = [ + { name: "Light", value: "vs-light" }, + { name: "Dark", value: "vs-dark" }, +]; + +// Events +export const UPDATE_ATTRIBS_EVT = "updateAttributes"; +export const SAVE_EVT = "save"; +export const RESET_EVT = "reset"; +export const DELETE_EVT = "delete"; +export const CHANGE_VERSION_EVT = "changeVersion"; +export const ASSIGN_MESSAGE_EVT = "assignMessage"; +export const MESSAGE_RESET_EVT = "messageReset"; +export const RESET_CONFIRM_EVT = "resetConfirm"; +export const CANCEL_EVENT_EVT = "cancel"; +export const DELETE_CONFIRM_EVT = "deleteConfirm"; +export const CHANGE_TAB_EVT = "changeTab"; +export const CHANGE_TAB_INNER_EVT = "changeTabInner"; +export const PERFORM_OPERATION_EVT = "performOperation"; +export const REPLACE_TASK_EVT = "replaceTask"; +export const DEBOUNCE_REPLACE_TASK_EVT = "debounceReplaceTask"; +export const REMOVE_TASK_EVT = "removeTask"; +export const ADD_NEW_SWITCH_PATH_EVT = "addNewSwitchPathToTask"; +export const UPDATE_WF_METADATA_EVT = "updateWorkflowMetadata"; +export const REMOVE_BRANCH_EVT = "removeBranch"; + +export const FLOW_FINISHED_RENDERING = "FLOW_FINISHED_RENDERING"; + +// Tabs +export const WORKFLOW_TAB = 0; +export const TASK_TAB = 1; +export const CODE_TAB = 2; +export const RUN_TAB = 3; +export const DEPENDENCIES_TAB = 4; + +// ERROR MESSAGES +export const SEVERITY_ERROR = "error"; + +export const WORKFLOW_DOES_NOT_EXIST_MESSAGE = + "Workflow definition does not exist, or you don't have permissions to view it."; diff --git a/ui-next/src/pages/definition/state/guards.ts b/ui-next/src/pages/definition/state/guards.ts new file mode 100644 index 0000000000..d00ae3987a --- /dev/null +++ b/ui-next/src/pages/definition/state/guards.ts @@ -0,0 +1,198 @@ +import { + WORKFLOW_TAB, + TASK_TAB, + CODE_TAB, + RUN_TAB, + DEPENDENCIES_TAB, +} from "./constants"; +import { + START_TASK_FAKE_TASK_REFERENCE_NAME, + END_TASK_FAKE_TASK_REFERENCE_NAME, +} from "components/flow/nodes"; +import { SelectNodeEvent } from "components/flow/state"; + +import _isNil from "lodash/isNil"; +import _last from "lodash/last"; +import { + DefinitionMachineContext, + ChangeTabEvent, + PerformOperationEvent, + SaveAndRunRequestEvent, + DONT_SHOW_IMPORT_SUCCESSFUL_DIALOG_TUTORIAL_AGAIN, +} from "./types"; +import { WorkflowWithNoErrorsEvent } from "../errorInspector/state"; +import { DoneInvokeEvent } from "xstate"; +import { ADD_TASK, ADD_TASK_ABOVE, ADD_TASK_BELOW } from "./taskModifier"; +import { TaskType } from "types"; +import fastDeepEqual from "fast-deep-equal"; +import { queryClient } from "queryClient"; +import { fetchContextNonHook } from "plugins/fetch"; +import { WORKFLOW_METADATA_BASE_URL_SHORT } from "utils/constants/api"; + +const fetchContext = fetchContextNonHook(); + +export const isNewWorkflow = (context: DefinitionMachineContext) => + context.isNewWorkflow; + +export const isEditorTab = ({ openedTab }: DefinitionMachineContext) => + openedTab === CODE_TAB; + +export const isRunTab = ({ openedTab }: DefinitionMachineContext) => + openedTab === RUN_TAB; + +export const isDependenciesTab = ({ openedTab }: DefinitionMachineContext) => + openedTab === DEPENDENCIES_TAB; + +export const comesFromCodeAimsTaskTabHasSelectedTask = ( + { openedTab, selectedTaskCrumbs }: DefinitionMachineContext, + { tab }: ChangeTabEvent, +) => + openedTab === CODE_TAB && tab === TASK_TAB && selectedTaskCrumbs?.length > 0; + +export const isTaskEditorTab = ({ openedTab }: DefinitionMachineContext) => + openedTab === TASK_TAB; + +export const isWorkflowEditorTab = ({ openedTab }: DefinitionMachineContext) => + openedTab === WORKFLOW_TAB; + +export const isDifferentTab = ( + { openedTab }: DefinitionMachineContext, + { tab }: ChangeTabEvent, +) => openedTab !== tab; + +export const isValidSelection = ( + _context: DefinitionMachineContext, + { node: { id } }: SelectNodeEvent, +) => + ![ + START_TASK_FAKE_TASK_REFERENCE_NAME, + END_TASK_FAKE_TASK_REFERENCE_NAME, + ].includes(id); + +export const isChangingTab = ( + { openedTab }: DefinitionMachineContext, + { tab }: ChangeTabEvent, +) => openedTab !== tab; + +export const hasLastPerformedOperation = ({ + lastPerformedOperation, +}: DefinitionMachineContext) => !_isNil(lastPerformedOperation); + +export const wasSaved = ( + _context: DefinitionMachineContext, + event: DoneInvokeEvent<{ saved: boolean }>, +) => event.data.saved; + +export const workflowWasSentWithNoErrors = ( + __context: DefinitionMachineContext, + event: WorkflowWithNoErrorsEvent, +) => event.workflow !== undefined; + +export const hasSelectedTask = ({ + selectedTaskCrumbs, +}: DefinitionMachineContext) => selectedTaskCrumbs.length === 0; + +export const wantToRemoveLastForkIndex = ({ + lastRemovalOperation, +}: DefinitionMachineContext) => { + return ( + lastRemovalOperation?.task.type === TaskType.FORK_JOIN && + lastRemovalOperation?.task?.forkTasks?.length === 1 && + lastRemovalOperation?.branchName === "0" + ); +}; + +export const isAddOperation = ( + __context: DefinitionMachineContext, + { data }: PerformOperationEvent, +) => { + return [ADD_TASK, ADD_TASK_ABOVE, ADD_TASK_BELOW].includes(data?.action); +}; + +export const isLastVersion = ( + context: DefinitionMachineContext, + event: DoneInvokeEvent<{ versions: string[] }>, +) => { + if (event.data.versions?.length === 0) { + return true; + } + return false; +}; + +export const selectedTaskIsInForkBranch = ({ + lastRemovalOperation, + selectedTaskCrumbs, +}: DefinitionMachineContext) => { + if (lastRemovalOperation?.task?.type === TaskType.FORK_JOIN) { + const lastCrumb = _last(selectedTaskCrumbs); + if (lastCrumb != null) { + return lastRemovalOperation.branchName === String(lastCrumb.forkIndex); + } + } + return false; +}; + +export const selectedTaskIsInSwitchBranch = ({ + lastRemovalOperation, + selectedTaskCrumbs, +}: DefinitionMachineContext) => { + if (lastRemovalOperation?.task?.type === TaskType.SWITCH) { + const lastCrumb = _last(selectedTaskCrumbs); + if (lastCrumb != null) { + return lastRemovalOperation.branchName === lastCrumb.decisionBranch; + } + } + return false; +}; + +export const isDescriptionEmpty = (context: DefinitionMachineContext) => + (context.workflowChanges?.description ?? "").trim() === ""; + +export const isSaveAndRunRequest = ( + __context: DefinitionMachineContext, + { isSaveAndRun }: SaveAndRunRequestEvent, +) => { + return isSaveAndRun ? true : false; +}; + +export const hasNoChanges = (context: DefinitionMachineContext) => + fastDeepEqual(context.workflowChanges, context.currentWf); + +export const isSaveAndRunWithNoChanges = ( + context: DefinitionMachineContext, + event: SaveAndRunRequestEvent, +) => { + return isSaveAndRunRequest(context, event) && hasNoChanges(context); +}; + +export const isFirstTimeFlow = (context: DefinitionMachineContext) => { + const workflowDefinitionUrl = WORKFLOW_METADATA_BASE_URL_SHORT; + const key = [fetchContext.stack, workflowDefinitionUrl]; + const data = queryClient.getQueryData(key); + // Check by local storage. and if there is any workflow in cache + return ( + Boolean(context.successfullyImportedWorkflowId) === true && + (data === undefined || data?.length === 0) + ); +}; + +export const dontNeedToShowImportSuccessfulDialog = ( + context: DefinitionMachineContext, +) => { + const dontShowImportSuccessfulDialogTutorialAgain = localStorage.getItem( + DONT_SHOW_IMPORT_SUCCESSFUL_DIALOG_TUTORIAL_AGAIN, + ); + return ( + Boolean(context.successfullyImportedWorkflowId) === false || + dontShowImportSuccessfulDialogTutorialAgain === "true" + ); +}; + +export const importSummaryHasDependencies = ( + context: DefinitionMachineContext, +) => { + return ( + context.importSummary?.integrationsAndModelsResponse != null && + context.importSummary?.integrationsAndModelsResponse.length > 0 + ); +}; diff --git a/ui-next/src/pages/definition/state/hook.ts b/ui-next/src/pages/definition/state/hook.ts new file mode 100644 index 0000000000..d1282f7328 --- /dev/null +++ b/ui-next/src/pages/definition/state/hook.ts @@ -0,0 +1,306 @@ +import { useInterpret, useSelector } from "@xstate/react"; +import { MessageContext } from "components/v1/layout/MessageContext"; +import { useSetAtom } from "jotai"; +import _get from "lodash/get"; +import { + DefinitionMachineEventTypes, + RedirectToExecutionPageEvent, + WorkflowDefinitionEvents, +} from "pages/definition/state/types"; +import { usePanelChanges } from "pages/definition/state/usePanelChanges"; +import { removeDeletedWorkflow } from "pages/runWorkflow/runWorkflowUtils"; +import { fetchWithContext, useFetchContext } from "plugins/fetch"; +import { useContext, useEffect, useMemo } from "react"; +import { useQueryClient } from "react-query"; +import { Location, useLocation, useNavigate, useParams } from "react-router"; +import { useQueryState } from "react-router-use-location-state"; +import { setDefinitionServiceAtom } from "shared/agent/agentAtomsStore"; +import { AuthContext } from "shared/auth/context"; +import { PersistableSidebarEventTypes } from "shared/PersistableSidebar/state/types"; +import { AuthProviderMachineContext } from "shared/state"; +import { User } from "types/User"; +import { getErrors } from "utils"; +import { WORKFLOW_METADATA_BASE_URL } from "utils/constants/api"; +import { WORKFLOW_DEFINITION_URL } from "utils/constants/route"; +import { useActionWithPath, useAuthHeaders } from "utils/query"; +import { ActorRef, State } from "xstate"; +import { workflowDefinitionMachine } from "./machine"; + +const WORKFLOW_FETCH_FAILED = "Failed to fetch workflow"; +const WORKFLOW_FETCH_FORBIDDEN = + "You don't seem to have access to view this workflow"; + +const isNewWorkflowFn = (location: Location) => + location.pathname === WORKFLOW_DEFINITION_URL.NEW; + +export const useWorkflowDefinition = (currentUser: User) => { + const queryClient = useQueryClient(); + const fetchContext = useFetchContext(); // Maintain compatibility + const { setMessage } = useContext(MessageContext); + const [timeInParameter, setTimeInParameter] = useQueryState( + "showImportSuccess", + "", + ); + const [blogUrl] = useQueryState("blogUrl", ""); + const [taskReferenceName, handleTaskReferenceName] = useQueryState( + "taskReferenceName", + "", + ); + const authHeaders = useAuthHeaders(); + + const setDefinitionService = useSetAtom(setDefinitionServiceAtom); + + // Needed stuff for compatibility mode + const navigate = useNavigate(); + + const { authService } = useContext(AuthContext); + + // No-op actor used as a fallback when authService is not available (OSS mode). + const dummyActor = useMemo( + (): ActorRef => ({ + id: "noop", + send: () => {}, + subscribe: () => ({ unsubscribe: () => {} }), + getSnapshot: () => ({ children: {} }), + [Symbol.observable]() { + return { subscribe: () => ({ unsubscribe: () => {} }) }; + }, + }), + [], + ); + + const sidebarActor = useSelector( + authService ?? dummyActor, + (state: State) => + state?.children?.["sidebarMachine"], + ); + + const { mutateAsync: deleteWorkflowMutator } = useActionWithPath(); + + const location = useLocation(); + const params = useParams(); + const isNewWorkflowUrl = isNewWorkflowFn(location); + const templateIdMaybe = _get(params, "templateId"); + const version = _get(params, "version"); + const workflowNameParam = _get(params, "name"); + const workflowName = useMemo(() => { + if (isNewWorkflowUrl) { + return "NEW"; + } + + if (workflowNameParam) { + try { + return decodeURIComponent(workflowNameParam); + } catch { + setMessage({ + severity: "error", + text: "Name has invalid chars and cant be opened.", + }); + return ""; + } + } + + return ""; + }, [isNewWorkflowUrl, workflowNameParam, setMessage]); + // End needed stuff + + const fetchWorkflowAndRelatedData = async ( + workflowName: string, + currentVersion: string | undefined, + ) => { + const maybeVersion = currentVersion ? `?version=${currentVersion}` : ""; + const path = `${WORKFLOW_METADATA_BASE_URL}/${encodeURIComponent( + workflowName, + )}${maybeVersion}`; + + try { + // First fetch the workflow metadata + const workflowData = await queryClient.fetchQuery( + [fetchContext.stack, path], + () => fetchWithContext(path, fetchContext, { headers: authHeaders }), + ); + + return { + workflow: workflowData, + }; + } catch (error: any) { + const errorMessage = (await getErrors(error))?.message; + + if (errorMessage) { + return Promise.reject({ message: errorMessage }); + } + + const status = error.status; + + if (status === 403) { + return Promise.reject({ message: WORKFLOW_FETCH_FORBIDDEN }); + } + + if (status === 404) { + return Promise.reject({ message: "Workflow was not found" }); + } + + return Promise.reject({ message: WORKFLOW_FETCH_FAILED }); + } + }; + + const service = useInterpret(workflowDefinitionMachine, { + ...(process.env.NODE_ENV === "development" ? { devTools: true } : {}), + context: { + authHeaders, + currentUserInfo: currentUser, + initialSelectedTaskReferenceName: taskReferenceName, + successfullyImportedWorkflowId: timeInParameter, + }, + services: { + fetchWorkflow: async (data) => { + const { workflowName, currentVersion } = data; + + if (workflowName) { + const result = fetchWorkflowAndRelatedData( + workflowName, + currentVersion, + ); + return result; + } + }, + deleteWorkflowVersion: async ({ currentWf }, __) => { + try { + if (currentWf?.name) { + // @ts-ignore + await deleteWorkflowMutator({ + method: "delete", + path: `${WORKFLOW_METADATA_BASE_URL}/${encodeURIComponent( + currentWf.name, + )}/${currentWf?.version}`, + }).then(() => { + removeDeletedWorkflow(currentWf?.name, currentWf?.version); + }); + } else { + return Promise.reject({ message: "Workflow's name is undefined" }); + } + } catch (error) { + return Promise.reject(error); + } + }, + }, + actions: { + closeLeftSidebar: () => { + sidebarActor?.send({ + type: PersistableSidebarEventTypes.COLLAPSE_SIDEBAR_EVENT, + }); + }, + openLeftSidebar: () => { + sidebarActor?.send({ + type: PersistableSidebarEventTypes.EXPAND_SIDEBAR_EVENT, + }); + }, + pushToHistory: ( + { workflowName, currentVersion }, + { isContinueCreate }: any, + ) => { + if (isContinueCreate) { + // Clear localStorage for new workflow when saving and creating new + localStorage.removeItem("newWorkflowDef"); + // Clear workflow history and selected workflow to ensure clean state + localStorage.removeItem("workflowHistory"); + localStorage.removeItem("selectedWorkflow"); + + if (isNewWorkflowUrl) { + window.location.reload(); + } else { + navigate(WORKFLOW_DEFINITION_URL.NEW); + } + } else if (workflowName) { + navigate( + `${WORKFLOW_DEFINITION_URL.BASE}/${encodeURIComponent( + workflowName, + )}${currentVersion == null ? "" : "/" + currentVersion}`, + ); + } + }, + goBackToDefinitionSelection: (_context) => { + navigate(WORKFLOW_DEFINITION_URL.BASE); + }, + redirectToExecutionPage: ( + _context, + data: RedirectToExecutionPageEvent, + ) => { + navigate(`/execution/${data.executionId}`); + }, + setQueryParam: (_context, data: any) => { + handleTaskReferenceName(data?.node?.id); + }, + removeQueryParam: (_context) => { + handleTaskReferenceName(""); + }, + dismissImportSuccessfullParam: () => { + setTimeInParameter(""); + }, + showSuccessMassage: () => { + setMessage({ + text: "Workflow saved successfully.", + severity: "success", + }); + }, + }, + }); + + const workflowVersions = useSelector( + service, + (state) => state.context.workflowVersions, + ); + + useEffect(() => { + if (isNewWorkflowUrl || templateIdMaybe || workflowName || version) { + service.send({ + type: DefinitionMachineEventTypes.UPDATE_ATTRIBS_EVT, + workflowName, + isNewWorkflow: isNewWorkflowUrl, + + currentVersion: version, + workflowTemplateId: templateIdMaybe, + }); + } + }, [workflowName, isNewWorkflowUrl, templateIdMaybe, version, service]); + + const handleSetMessage = (messageSeverity: any) => + service.send({ + type: DefinitionMachineEventTypes.ASSIGN_MESSAGE_EVT, + ...messageSeverity, + }); + + const handleResetMessage = () => { + service.send({ type: DefinitionMachineEventTypes.MESSAGE_RESET_EVT }); + }; + + const isNewWorkflow = useSelector( + service, + (state) => state.context.isNewWorkflow, + ); + + const message = useSelector(service, (state) => state.context.message); + const { leftPanelExpanded, setLeftPanelExpanded } = usePanelChanges(service); + + // FIXME: Temporary hack to pin the service to the Agent atom store. + useEffect(() => { + setDefinitionService(service as ActorRef); + }, [service, setDefinitionService]); + + return [ + { + handleSetMessage, + handleResetMessage, + setLeftPanelExpanded, + }, + { + isNewWorkflow, + workflowName, + workflowVersions, + message, + definitionActor: service, + leftPanelExpanded, + blogUrl, + }, + ] as const; +}; diff --git a/ui-next/src/pages/definition/state/index.ts b/ui-next/src/pages/definition/state/index.ts new file mode 100644 index 0000000000..eceac8299f --- /dev/null +++ b/ui-next/src/pages/definition/state/index.ts @@ -0,0 +1,2 @@ +export * from "./types"; +export * from "./WorkflowEditContext"; diff --git a/ui-next/src/pages/definition/state/machine.ts b/ui-next/src/pages/definition/state/machine.ts new file mode 100644 index 0000000000..a457c6d0e2 --- /dev/null +++ b/ui-next/src/pages/definition/state/machine.ts @@ -0,0 +1,1271 @@ +import { crumbsToTaskSteps } from "components/flow/nodes"; +import { FlowActionTypes } from "components/flow/state"; +import { flowMachine } from "components/flow/state/machine"; +import _flow from "lodash/flow"; +import _isEmpty from "lodash/isEmpty"; +import _last from "lodash/last"; +import _prop from "lodash/property"; +import { assign, createMachine, spawn } from "xstate"; +import { + localCopyMachine, + LocalCopyMachineEventTypes, + removeCopyFromStorage, +} from "../ConfirmLocalCopyDialog/state"; +import { + codeMachine, + CodeMachineEventTypes, +} from "../EditorPanel/CodeEditorTab/state"; +import { formMachine } from "../EditorPanel/TaskFormTab/state"; +import { IdempotencyStrategyEnum, runMachine } from "../RunWorkflow/state"; +import { workflowMetadataMachine } from "../WorkflowMetadata/state"; +import { + saveMachine, + SaveWorkflowMachineEventTypes, +} from "../confirmSave/state"; +import { + ErrorInspectorEventTypes, + errorInspectorMachine, +} from "../errorInspector/state"; +import { extractWorkflowMetadata } from "../helpers"; +import * as actions from "./action"; +import { WORKFLOW_TAB } from "./constants"; +import * as guards from "./guards"; +import { + fetchForImportedTemplateImportSummary, + fetchInputSchema, + fetchSecretsEndEnvironmentsList, + persistCopyInLocalStorage, + refetchCurrentWorkflowVersionsService, +} from "./services"; +import { + DefinitionMachineContext, + DefinitionMachineEventTypes, + WorkflowDefinitionEvents, +} from "./types"; + +const selectedTaskReferenceName = _flow([_last, _prop("ref")]); + +export const workflowDefinitionMachine = createMachine< + DefinitionMachineContext, + WorkflowDefinitionEvents +>( + { + predictableActionArguments: true, + id: "workflowDefinitionMachine", + initial: "init", + context: { + successfullyImportedWorkflowId: undefined, + currentWf: undefined, + workflowChanges: {}, + isNewWorkflow: false, + workflowName: undefined, + currentVersion: undefined, + workflowVersions: [], + selectedTaskCrumbs: [], + authHeaders: {}, // This should be in auth actor + message: { + text: undefined, + severity: undefined, + }, + localCopyMessage: undefined, + openedTab: WORKFLOW_TAB, + previousTab: WORKFLOW_TAB, + lastPerformedOperation: undefined, + errorInspectorMachine: undefined, + downloadFileObj: undefined, + lastRemovalOperation: undefined, + workflowTemplateId: undefined, + collapseWorkflowList: [], + isAgentExpanded: false, + }, + on: { + [DefinitionMachineEventTypes.UPDATE_ATTRIBS_EVT]: { + actions: ["persistWorkflowAttribs"], + target: "fetchForVersions", + }, + [DefinitionMachineEventTypes.TOGGLE_META_BAR_EDIT_MODE_EVT]: { + actions: ["toggleMetaBarEditMode"], + }, + [DefinitionMachineEventTypes.TOGGLE_AGENT_EXPANDED]: { + actions: ["toggleAgentExpanded"], + }, + }, + states: { + fetchForVersions: { + invoke: { + src: "refetchCurrentWorkflowVersionsService", + onDone: { + actions: ["persistWorkflowVersionsParsed"], + target: "checkLocalStorage", + }, + }, + }, + init: { + entry: assign({ + errorInspectorMachine: (ctx: DefinitionMachineContext) => + spawn( + errorInspectorMachine.withContext({ + authHeaders: ctx.authHeaders, + currentWf: undefined, + workflowErrors: [], + taskErrors: [], + serverErrors: [], + crumbMap: undefined, + workflowReferenceProblems: [], + taskReferencesProblems: [], + unreachableTaskProblems: [], + runWorkflowErrors: [], + }), + "errorInspectorMachine", + ), + // @ts-ignore + flowMachine: (ctx: DefinitionMachineContext) => + spawn( + flowMachine.withContext({ + authHeaders: ctx.authHeaders, + currentWf: {}, + selectedNodeIdx: undefined, + nodes: [], + edges: [], + menuOperationContext: undefined, + openedNode: undefined, + }), + "flowMachine", + ), + }), + }, + checkLocalStorage: { + invoke: { + id: "localCopyMachine", + src: localCopyMachine, + data: { + lastStoredVersion: ({ + workflowChanges, + }: DefinitionMachineContext) => workflowChanges, + workflowName: ({ workflowName }: DefinitionMachineContext) => + workflowName, + currentVersion: ({ currentVersion }: DefinitionMachineContext) => + currentVersion, + isNewWorkflow: ({ isNewWorkflow }: DefinitionMachineContext) => + isNewWorkflow, + currentWf: ({ currentWf }: DefinitionMachineContext) => currentWf, + }, + onDone: { + actions: [ + "updateWfFromLocalStorage", + "maybePersistLocalCopyMessage", + ], + target: ["presentEditor"], + }, + onError: {}, + }, + entry: "forwardWorkflowToLocalCopyMachine", + }, + presentEditor: { + always: [ + // condition has changes from localStorage send to ready\ + { + cond: "isNewWorkflow", + target: "ready", + }, + + { + target: "fetchForWorkflow", + cond: ({ + currentVersion, + workflowVersions, + }: DefinitionMachineContext) => + currentVersion !== null || workflowVersions?.length > 1, + }, + { + target: "backToList", + cond: ({ currentVersion }: DefinitionMachineContext) => + currentVersion === null, + }, + ], + }, + backToList: { + initial: "waitForActions", + states: { + waitForActions: { + after: { + 100: "goBack", + }, + }, + goBack: { + entry: "goBackToDefinitionSelection", + type: "final", + }, + }, + }, + fetchForWorkflow: { + invoke: { + src: "fetchWorkflow", + onDone: { + actions: ["updateWf"], + target: "fetchForInputSchema", + }, + onError: { + actions: ["processErrorFetching"], + target: "errorFetchingWorkflow", + }, + }, + }, + fetchForInputSchema: { + invoke: { + src: "fetchInputSchema", + onDone: { + actions: ["updateWfDefaultRunParam"], + target: "ready", + }, + onError: { + target: "ready", + }, + }, + }, + ready: { + entry: "sendWorkflowToInspector", + type: "parallel", + states: { + agentAutoExpand: { + initial: "waiting", + states: { + waiting: { + after: { + // Delay expansion to allow workflow diagram to render first + 400: { + target: "expanded", + actions: "autoExpandAgentForNewWorkflow", + cond: "isNewWorkflow", + }, + }, + }, + expanded: { + type: "final", + }, + }, + }, + rightPanel: { + initial: "opened", + on: { + [DefinitionMachineEventTypes.PERFORM_OPERATION_EVT]: { + target: + "#workflowDefinitionMachine.ready.rightPanel.opened.pendingSelections", + cond: "isAddOperation", + }, + [FlowActionTypes.SELECT_NODE_EVT]: { + target: + "#workflowDefinitionMachine.ready.rightPanel.opened.tabFocus", + cond: "isValidSelection", + }, + [DefinitionMachineEventTypes.SYNC_RUN_CONTEXT_AND_CHANGE_TAB]: { + actions: [ + "updateRunTabFormState", + "changeTab", + "gtagEventLogger", + "cleanRunErrors", + ], + target: + "#workflowDefinitionMachine.ready.rightPanel.opened.tabFocus", + }, + [DefinitionMachineEventTypes.COLLAPSE_SIDEBAR_AND_RIGHT_PANEL]: { + actions: ["closeLeftSidebar"], + target: "#workflowDefinitionMachine.ready.rightPanel.closed", + }, + }, + states: { + opened: { + initial: "pendingSelections", + states: { + refetchCurrentWorkflowVersions: { + invoke: { + src: "refetchCurrentWorkflowVersionsService", + id: "refetch-wf-versions", + onError: { + target: "#workflowDefinitionMachine.backToList", + actions: ["logStuff"], + }, + onDone: [ + { + cond: "isLastVersion", + actions: ["resetChanges"], //reset changes so we dont get trapped in the dialog + target: "#workflowDefinitionMachine.backToList", + }, + { + actions: ["persistLatestVersion", "pushToHistory"], + }, + ], + }, + }, + deleteWorkflow: { + invoke: { + src: "deleteWorkflowVersion", + id: "delete-workflow", + onError: { + actions: ["logStuff"], + }, + onDone: { + actions: ["removeLocalCopy", "resetCurrentVersion"], + target: "refetchCurrentWorkflowVersions", + }, + }, + }, + + pendingSelections: { + always: [ + { + target: "addressPendingSelections", + cond: "hasLastPerformedOperation", + }, + { target: "tabFocus" }, + ], + }, + addressPendingSelections: { + on: { + [DefinitionMachineEventTypes.FLOW_FINISHED_RENDERING]: { + actions: ["selectNewTask", "cleanLastOperation"], + target: "tabFocus", + }, + [DefinitionMachineEventTypes.CHANGE_TAB_EVT]: { + // In case something fails allow user to change tabs + actions: [ + "changeTab", + "cleanLastOperation", + "gtagEventLogger", + ], + target: "tabFocus", + }, + }, + }, + codeEditor: { + exit: ["cleanCodeTextReference", "cleanSerializationError"], + invoke: { + src: codeMachine, + id: "codeMachine", + data: { + originalWorkflow: ({ + currentWf, + }: DefinitionMachineContext) => currentWf, + editorChanges: ({ + workflowChanges, + }: DefinitionMachineContext) => + JSON.stringify(workflowChanges, null, 2), + errorInspectorMachine: ({ + errorInspectorMachine, + }: DefinitionMachineContext) => errorInspectorMachine, + referenceText: ({ + selectedTaskCrumbs, + codeTextReference, + }: DefinitionMachineContext) => { + // Has a persisted error + if (codeTextReference) { + return codeTextReference; + } + + // maybe has a selected task + const selectedTaskReference = + selectedTaskReferenceName(selectedTaskCrumbs || []); + if (selectedTaskReference) { + return { + textReference: selectedTaskReference, + referenceReason: "info", + }; + } + return undefined; + }, + }, + }, + on: { + [ErrorInspectorEventTypes.WORKFLOW_WITH_NO_ERRORS]: { + actions: [ + "persistWorkflowChanges", + "notifyFlowUpdates", + "startRenderingGtag", + ], + cond: "workflowWasSentWithNoErrors", + }, + [ErrorInspectorEventTypes.WORKFLOW_HAS_ERRORS]: { + actions: ["persistWorkflowChanges"], + cond: "workflowWasSentWithNoErrors", + }, + [DefinitionMachineEventTypes.CHANGE_TAB_EVT]: [ + { + cond: "comesFromCodeAimsTaskTabHasSelectedTask", + actions: ["reSelectTaskIfSelected"], + target: "tabFocus", + }, + { + actions: ["changeTab"], + cond: "isDifferentTab", + target: "tabFocus", + }, + ], + [CodeMachineEventTypes.HIGHLIGHT_TEXT_REFERENCE]: { + actions: "forwardToCodeMachine", + }, + [CodeMachineEventTypes.JUMP_TO_FIRST_ERROR]: { + actions: "forwardToCodeMachine", + }, + }, + }, + taskEditor: { + invoke: { + id: "formTaskMachine", + src: formMachine, + data: ({ + selectedTaskCrumbs, + workflowChanges, + authHeaders, + }: DefinitionMachineContext) => { + const tasksBranch = _isEmpty(selectedTaskCrumbs) + ? [] + : crumbsToTaskSteps( + selectedTaskCrumbs, + workflowChanges?.tasks || [], + ); + const crumbs = selectedTaskCrumbs; + const workflowInputParameters = + workflowChanges?.inputParameters; + const originalTask = _last(tasksBranch); + return { + originalTask, + taskChanges: originalTask, + crumbs, + tasksBranch, + workflowInputParameters, + authHeaders, + }; + }, + }, + on: { + [ErrorInspectorEventTypes.WORKFLOW_WITH_NO_ERRORS]: { + actions: ["forwardCleanWorkflow", "sendCrumbUpdates"], + cond: "workflowWasSentWithNoErrors", + }, + [ErrorInspectorEventTypes.WORKFLOW_HAS_ERRORS]: { + actions: ["setFlowAsReadOnly"], + cond: "workflowWasSentWithNoErrors", + }, + [DefinitionMachineEventTypes.REPLACE_TASK_EVT]: { + actions: [ + "replaceTask", + "validateWorkflow", + "updateSelectedCrumbs", + "gtagEventLogger", + ], + }, + [DefinitionMachineEventTypes.CHANGE_TAB_EVT]: { + actions: ["changeTab", "gtagEventLogger"], + cond: "isDifferentTab", + target: "tabFocus", + }, + [FlowActionTypes.SELECT_NODE_EVT]: { + target: "tabFocusAfter", + cond: "isValidSelection", + }, + [FlowActionTypes.SELECT_EDGE_EVT]: { + actions: ["forwardSelectEdge"], + }, + [FlowActionTypes.UPDATE_COLLAPSE_WORKFLOW_LIST]: { + actions: ["forwardCollapseWorkflowList"], + }, + [DefinitionMachineEventTypes.FLOW_FINISHED_RENDERING]: { + actions: ["updateCollapseWorkflowList"], + }, + }, + }, + runWorkflow: { + tags: ["runWorkflowTab"], + invoke: { + id: "runWorkflowMachine", + src: runMachine, + data: { + authHeaders: ({ + authHeaders, + }: DefinitionMachineContext) => authHeaders, + currentWf: ({ + currentWf, + workflowName, + }: DefinitionMachineContext) => ({ + ...currentWf, + name: workflowName, // This allows running shared workflows. we only update workflowName when workflow is saved + }), + errorInspectorMachine: ({ + errorInspectorMachine, + }: DefinitionMachineContext) => errorInspectorMachine, + input: ({ + runTabFormState, + }: DefinitionMachineContext) => + runTabFormState?.input ?? "{}", + workflowDefaultRunParam: ({ + workflowDefaultRunParam, + }: DefinitionMachineContext) => workflowDefaultRunParam, + correlationId: ({ + runTabFormState, + }: DefinitionMachineContext) => + runTabFormState?.correlationId ?? undefined, + taskToDomain: ({ + runTabFormState, + }: DefinitionMachineContext) => + runTabFormState?.taskToDomain ?? "{}", + idempotencyKey: ({ + runTabFormState, + }: DefinitionMachineContext) => + runTabFormState?.idempotencyKey ?? undefined, + idempotencyStrategy: ({ + runTabFormState, + }: DefinitionMachineContext) => + runTabFormState?.idempotencyStrategy ?? + IdempotencyStrategyEnum.RETURN_EXISTING, + }, + }, + on: { + [DefinitionMachineEventTypes.CHANGE_TAB_EVT]: { + actions: ["forwardToRunWorkflowMachine"], + cond: "isDifferentTab", + }, + [DefinitionMachineEventTypes.REDIRECT_TO_EXECUTION_PAGE]: + { + actions: ["redirectToExecutionPage"], + }, + }, + }, + dependencies: { + tags: ["dependenciesTab"], + + on: { + [DefinitionMachineEventTypes.CHANGE_TAB_EVT]: { + actions: ["changeTab", "gtagEventLogger"], + cond: "isDifferentTab", + target: "tabFocus", + }, + [DefinitionMachineEventTypes.REDIRECT_TO_EXECUTION_PAGE]: + { + actions: ["redirectToExecutionPage"], + }, + }, + }, + tabFocusAfter: { + // always type transitions wont target an actual transition. If condition did not change + // We want the transition to happen to get a re-render + after: { + 50: { target: "tabFocus" }, + }, + }, + workflowEditor: { + invoke: { + src: workflowMetadataMachine, + id: "workflowTabMetaEditor", + data: { + metadata: (context: DefinitionMachineContext) => + extractWorkflowMetadata(context.workflowChanges!), + metadataChanges: (context: DefinitionMachineContext) => + extractWorkflowMetadata(context.workflowChanges!), + authHeaders: (context: DefinitionMachineContext) => + context.authHeaders, + editableFields: [ + "inputParameters", + "outputParameters", + "restartable", + "timeoutSeconds", + "timeoutPolicy", + "failureWorkflow", + "name", + "description", + "inputSchema", + "outputSchema", + "enforceSchema", + "workflowStatusListenerEnabled", + "workflowStatusListenerSink", + "rateLimitConfig", + ], + childActorsMachineName: "metadataFieldMachine", + }, + }, + on: { + [DefinitionMachineEventTypes.CHANGE_TAB_EVT]: { + actions: ["changeTab", "gtagEventLogger"], + cond: "isDifferentTab", + target: "tabFocus", + }, + [LocalCopyMachineEventTypes.USE_LOCAL_COPY_WORKFLOW]: { + actions: [ + "forwardWorkflowToTabMetadataEditorMachine", + "forwardWorkflowToMetadataEditorMachine", + ], + }, + [DefinitionMachineEventTypes.UPDATE_WF_METADATA_EVT]: { + actions: [ + "updateWFMetadata", + "validateWorkflow", + "notifyToFlowIfOutputParameters", + "gtagEventLogger", + ], // Will notify flow only if outputParameters were modified, else log + }, + [DefinitionMachineEventTypes.RESET_CONFIRM_EVT]: { + actions: [ + "sendWorkflowChangesToMetadataMachine", + "cleanLocalCopyMessage", + "gtagEventLogger", + ], + }, + }, + }, + tabFocus: { + always: [ + { target: "codeEditor", cond: "isEditorTab" }, + { target: "taskEditor", cond: "isTaskEditorTab" }, + { + target: "workflowEditor", + cond: "isWorkflowEditorTab", + }, + { target: "runWorkflow", cond: "isRunTab" }, + { target: "dependencies", cond: "isDependenciesTab" }, + ], + }, + confirmReset: { + on: { + [DefinitionMachineEventTypes.RESET_CONFIRM_EVT]: { + actions: [ + "resetChanges", // Go back to old version + "cleanServerErrors", // Clean server errors + "removeLocalCopy", // Tell actor to remove local copy + "sendWorkflowToInspector", // Tell actor what the new workflow is + "notifyFlowUpdates", // Tell actor to redo the nodes for the new changes + "notifyFlowResetZoomPosition", // Tell actor to reset zoom, position of diagram + "startRenderingGtag", + "cleanLocalCopyMessage", + ], + target: "tabFocus", + }, + [DefinitionMachineEventTypes.CANCEL_EVENT_EVT]: { + target: "tabFocus", + actions: ["gtagEventLogger"], + }, + }, + }, + saveRunRequest: { + tags: ["saveRequest"], + initial: "confirmSave", + states: { + confirmSave: { + entry: ["setFlowAsReadOnly"], + invoke: { + src: "saveMachine", + id: "saveChangesMachine", + onDone: {}, + onError: { + target: + "#workflowDefinitionMachine.ready.rightPanel.opened.tabFocus", + }, + data: { + editorChanges: ({ + workflowChanges, + }: DefinitionMachineContext) => + JSON.stringify(workflowChanges, null, 2), + workflowName: ({ + workflowName, + }: DefinitionMachineContext) => workflowName, + currentVersion: ({ + currentVersion, + }: DefinitionMachineContext) => currentVersion, + isNewWorkflow: ({ + isNewWorkflow, + }: DefinitionMachineContext) => isNewWorkflow, + currentWf: ({ + currentWf, + }: DefinitionMachineContext) => currentWf, + authHeaders: ({ + authHeaders, + }: DefinitionMachineContext) => authHeaders, + errorInspectorMachine: ({ + errorInspectorMachine, + }: DefinitionMachineContext) => + errorInspectorMachine, + isNewVersion: ( + __context: DefinitionMachineContext, + { isNewVersion }: { isNewVersion: boolean }, + ) => isNewVersion, + isContinueCreate: ( + __context: DefinitionMachineContext, + { + originalEvent, + }: { originalEvent: DefinitionMachineEventTypes }, + ) => + originalEvent === + DefinitionMachineEventTypes.HANDLE_SAVE_AND_CREATE_NEW, + }, + }, + on: { + [SaveWorkflowMachineEventTypes.SAVED_CANCELLED]: { + target: + "#workflowDefinitionMachine.ready.rightPanel.opened.tabFocus", + actions: [ + "changeToPreviousTab", + "persistWorkflowChanges", + "notifyFlowUpdates", + ], + }, + [SaveWorkflowMachineEventTypes.SAVED_SUCCESSFUL]: { + target: + "#workflowDefinitionMachine.ready.rightPanel.opened.runWorkflow", + actions: [ + "showSuccessMassage", + "cleanLastOperation", + "persistWorkflowNameAndVersion", + "cleanLocalCopyMessage", // Note push to history has a callback event since the url will change. + "cleanInitialSelectedTaskReferenceName", + "justExecute", + ], + }, + }, + }, + }, + }, + saveRequest: { + tags: ["saveRequest"], + initial: "confirmSave", + states: { + confirmSave: { + entry: ["setFlowAsReadOnly"], + invoke: { + src: "saveMachine", + id: "saveChangesMachine", + onDone: {}, + onError: { + target: + "#workflowDefinitionMachine.ready.rightPanel.opened.tabFocus", + }, + data: { + editorChanges: ({ + workflowChanges, + }: DefinitionMachineContext) => + JSON.stringify(workflowChanges, null, 2), + workflowName: ({ + workflowName, + }: DefinitionMachineContext) => workflowName, + currentVersion: ({ + currentVersion, + }: DefinitionMachineContext) => currentVersion, + isNewWorkflow: ({ + isNewWorkflow, + }: DefinitionMachineContext) => isNewWorkflow, + currentWf: ({ + currentWf, + }: DefinitionMachineContext) => currentWf, + authHeaders: ({ + authHeaders, + }: DefinitionMachineContext) => authHeaders, + errorInspectorMachine: ({ + errorInspectorMachine, + }: DefinitionMachineContext) => + errorInspectorMachine, + isNewVersion: ( + __context: DefinitionMachineContext, + { isNewVersion }: { isNewVersion: boolean }, + ) => isNewVersion, + isContinueCreate: ( + __context: DefinitionMachineContext, + { + originalEvent, + }: { originalEvent: DefinitionMachineEventTypes }, + ) => + originalEvent === + DefinitionMachineEventTypes.HANDLE_SAVE_AND_CREATE_NEW, + }, + }, + on: { + [SaveWorkflowMachineEventTypes.CANCEL_SAVE_EVT]: { + actions: "forwardToSaveMachine", + }, + [SaveWorkflowMachineEventTypes.SAVED_CANCELLED]: { + target: + "#workflowDefinitionMachine.ready.rightPanel.opened.tabFocus", + actions: [ + "changeToPreviousTab", + "persistWorkflowChanges", + "notifyFlowUpdates", + "startRenderingGtag", + ], + }, + [SaveWorkflowMachineEventTypes.SAVED_SUCCESSFUL]: { + target: "#workflowDefinitionMachine.presentEditor", + actions: [ + "showSuccessMassage", + "cleanLastOperation", + "changeToPreviousTab", + "persistWorkflowNameAndVersion", + "cleanLocalCopyMessage", // Note push to history has a callback event since the url will change. + "cleanInitialSelectedTaskReferenceName", + "pushToHistory", + "raiseUpdateAtribsEvent", + ], + }, + }, + }, + }, + }, + confirmDelete: { + on: { + [DefinitionMachineEventTypes.DELETE_CONFIRM_EVT]: { + target: "deleteWorkflow", + }, + [DefinitionMachineEventTypes.CANCEL_EVENT_EVT]: { + target: "tabFocus", + }, + }, + }, + }, + on: { + [DefinitionMachineEventTypes.SAVE_EVT]: [ + { + cond: "isDescriptionEmpty", + actions: ["fireChangeToWorkflowTab"], + }, + { + cond: "isSaveAndRunWithNoChanges", + target: ".runWorkflow", + actions: ["justExecute"], + // actions: ["fireChangeToRunTab"], + }, + { + cond: "isSaveAndRunRequest", + target: ".saveRunRequest", + actions: ["changeToCodeTab"], + }, + { + target: ".saveRequest", + actions: ["changeToCodeTab", "gtagEventLogger"], + }, + ], + [DefinitionMachineEventTypes.HANDLE_SAVE_AND_RUN]: { + actions: ["fireSaveEvent"], + }, + [DefinitionMachineEventTypes.HANDLE_SAVE_AND_CREATE_NEW]: { + actions: [ + "setSaveSourceEventType", + "fireSaveAndCreateNewRequestEvent", + ], + }, + [DefinitionMachineEventTypes.RESET_EVT]: { + target: ".confirmReset", + actions: ["gtagEventLogger"], + }, + [DefinitionMachineEventTypes.DELETE_EVT]: { + target: ".confirmDelete", + actions: ["gtagEventLogger"], + }, + [DefinitionMachineEventTypes.CHANGE_VERSION_EVT]: { + actions: ["setVersion", "pushToHistory", "gtagEventLogger"], + }, + [DefinitionMachineEventTypes.ASSIGN_MESSAGE_EVT]: { + actions: ["setMessage", "gtagEventLogger"], + }, + [DefinitionMachineEventTypes.MESSAGE_RESET_EVT]: { + actions: ["resetMessage", "gtagEventLogger"], + }, + [DefinitionMachineEventTypes.REMOVE_TASK_EVT]: { + actions: [ + "removeTask", + "cleanTaskCrumbSelection", + "notifyFlowUpdates", + "changeToPreviousTab", + "startRenderingGtag", + "removeQueryParam", + ], + target: ".tabFocus", + }, + [CodeMachineEventTypes.HIGHLIGHT_TEXT_REFERENCE]: { + actions: ["persistCodeReference", "fireChangeToCodeTab"], + }, + [DefinitionMachineEventTypes.HANDLE_LEFT_PANEL_EXPANDED]: { + target: "closed", + cond: (_, data: any) => (data?.onSelectNode ? false : true), + }, + }, + }, + closed: { + on: { + [DefinitionMachineEventTypes.HANDLE_LEFT_PANEL_EXPANDED]: { + target: "opened", + }, + [DefinitionMachineEventTypes.RESET_EVT]: { + actions: ["raiseResetEvent"], + target: "opened", + }, + [DefinitionMachineEventTypes.DELETE_EVT]: { + actions: ["raiseDeleteEvent"], + target: "opened", + }, + [DefinitionMachineEventTypes.SAVE_EVT]: { + actions: ["raiseSaveEvent"], + target: "opened", + }, + [DefinitionMachineEventTypes.HANDLE_SAVE_AND_RUN]: { + actions: ["raiseSaveAndRunEvent"], + target: "opened", + }, + [DefinitionMachineEventTypes.HANDLE_SAVE_AND_CREATE_NEW]: { + actions: [ + "setSaveSourceEventType", + "raiseSaveAndCreateNewEvent", + ], + target: "opened", + }, + [DefinitionMachineEventTypes.CHANGE_VERSION_EVT]: { + actions: ["setVersion", "pushToHistory", "gtagEventLogger"], + }, + }, + }, + }, + }, + diagram: { + entry: "notifyFlowUpdates", + initial: "rendered", + states: { + validateAndNotifyUpdates: { + entry: ["validateWorkflow"], + on: { + [ErrorInspectorEventTypes.WORKFLOW_WITH_NO_ERRORS]: { + actions: ["notifyFlowUpdates", "startRenderingGtag"], + cond: "workflowWasSentWithNoErrors", + target: "rendered", + }, + [ErrorInspectorEventTypes.WORKFLOW_HAS_ERRORS]: { + actions: ["setFlowAsReadOnly"], + cond: "workflowWasSentWithNoErrors", + target: "rendered", + }, + }, + }, + rendered: { + on: { + [DefinitionMachineEventTypes.REMOVE_BRANCH_EVT]: { + actions: [ + "persistRemovalOperation", + "fireChangeToWorkflowTab", + "gtagEventLogger", + ], + target: "branchRemoval", + }, + [DefinitionMachineEventTypes.ADD_NEW_SWITCH_PATH_EVT]: { + actions: ["addNewSwitchStatementToTask", "gtagEventLogger"], + target: "validateAndNotifyUpdates", + }, + [DefinitionMachineEventTypes.PERFORM_OPERATION_EVT]: { + actions: [ + "performOperation", + "persistLastOperation", + "gtagEventLogger", + ], + target: "validateAndNotifyUpdates", + }, + [ErrorInspectorEventTypes.REPORT_FLOW_ERROR]: { + actions: "forwardToErrorInspector", + }, + [DefinitionMachineEventTypes.FLOW_FINISHED_RENDERING]: { + actions: [ + "forwardToErrorInspector", + "maybeSelectInitialSelectedTaskReference", + "cleanInitialSelectedTaskReferenceName", + ], + }, + [DefinitionMachineEventTypes.MOVE_TASK_EVT]: { + actions: ["moveTaskFromLocation", "selectMovedTask"], + target: "validateAndNotifyUpdates", + }, + [FlowActionTypes.SELECT_NODE_EVT]: { + actions: [ + "persistSelectedTabCrumbs", + "changeToTaskTab", + "handleLeftPanelExpanded", + "setQueryParam", + ], + cond: "isValidSelection", + }, + }, + }, + branchRemoval: { + // TODO This looks like it could be extracted to a machine + // if no forks then the forkJoin makes no sense. + initial: "removalMakesSense", + states: { + removalMakesSense: { + always: [ + { + cond: "wantToRemoveLastForkIndex", + target: "confirmForkJoinRemoval", + }, + { + cond: "selectedTaskIsInForkBranch", + target: "switchToWorkflowTab", + }, + { + cond: "selectedTaskIsInSwitchBranch", + target: "switchToWorkflowTab", + }, + { + target: "cleanAndRender", + }, + ], + }, + switchToWorkflowTab: { + entry: [ + "fireChangeToWorkflowTab", + "cleanTaskCrumbSelection", + ], + always: "cleanAndRender", + }, + cleanAndRender: { + entry: [ + "removeBranchFromTask", + "cleanLastRemovalOperation", + "reSelectTaskIfSelected", + ], + always: + "#workflowDefinitionMachine.ready.diagram.validateAndNotifyUpdates", + }, + confirmForkJoinRemoval: { + on: { + [DefinitionMachineEventTypes.CONFIRM_LAST_FORK_REMOVAL]: { + actions: [ + "applyLastRemovalOperationAsRemoveTaskOperation", + "validateWorkflow", + "cleanLastRemovalOperation", + "gtagEventLogger", + ], + target: + "#workflowDefinitionMachine.ready.diagram.validateAndNotifyUpdates", + }, + [DefinitionMachineEventTypes.CANCEL_EVENT_EVT]: { + actions: ["cleanLastRemovalOperation"], + target: + "#workflowDefinitionMachine.ready.diagram.rendered", + }, + }, + }, + }, + }, + }, + }, + localCopiesKeeper: { + on: { + [ErrorInspectorEventTypes.WORKFLOW_WITH_NO_ERRORS]: { + cond: "workflowWasSentWithNoErrors", + target: ".storeInLocalStorage", + }, + [LocalCopyMachineEventTypes.REMOVE_LOCAL_COPY_MESSAGE]: { + actions: ["cleanLocalCopyMessage"], + }, + [LocalCopyMachineEventTypes.REMOVE_LOCAL_COPY]: { + target: ".removeWorkflowFromStorage", + }, + }, + initial: "cleanLocalCopyMessage", + states: { + idle: {}, + storeInLocalStorage: { + invoke: { + src: "persistCopyInLocalStorage", + onDone: { + target: "idle", + }, + }, + }, + removeWorkflowFromStorage: { + invoke: { + src: "removeCopyFromStorage", + onDone: { + target: "idle", + }, + }, + }, + cleanLocalCopyMessage: { + after: { + 5000: { + actions: ["cleanLocalCopyMessage"], + target: "idle", + }, + }, + }, + }, + }, + fetchForSecrets: { + initial: "fetchSecretsList", + states: { + idle: {}, + fetchSecretsList: { + invoke: { + src: "fetchSecretsEndEnvironmentsList", + id: "fetch-secrets-and-envs", + onDone: { + actions: ["updateSecretsAndEnvs"], + target: "idle", + }, + onError: { + actions: ["logStuff"], + target: "idle", + }, + }, + }, + }, + }, + importSuccessfulFlow: { + initial: "pullImportSummary", + states: { + idle: { + entry: ["dismissImportSuccessfullParam"], + }, + pullImportSummary: { + invoke: { + src: "fetchForImportedTemplateImportSummary", + onDone: [ + { + cond: (_, { data }) => data != null, + actions: ["persistImportSummary"], + target: "checkFirstTimeFlow", + }, + { + target: "checkFirstTimeFlow", + }, + ], + }, + }, + checkFirstTimeFlow: { + always: [ + { + cond: "dontNeedToShowImportSuccessfulDialog", + target: "idle", + }, + { + target: "closeLeftSidebar", + }, + ], + }, + closeLeftSidebar: { + entry: ["closeLeftSidebar"], + after: { + 500: { + target: "showImportSuccessfulFlow", + }, + }, + }, + showImportSuccessfulFlow: { + initial: "showCongratsMessage", + on: { + [DefinitionMachineEventTypes.DISMISS_IMPORT_SUCCESSFUL_DIALOG]: + { + target: "idle", + }, + }, + states: { + idle: { + entry: [ + "dismissImportSuccessfullParam", + "markDontShowImportSuccessfulDialogAgain", + ], + }, + showCongratsMessage: { + tags: ["showCongratsMessage"], + on: { + [DefinitionMachineEventTypes.NEXT_STEP_IMPORT_SUCCESSFUL_DIALOG]: + { + target: "showRunMessage", + actions: ["fireChangeToRunTab"], + }, + }, + }, + showRunMessage: { + tags: ["showRunMessage"], + on: { + [DefinitionMachineEventTypes.NEXT_STEP_IMPORT_SUCCESSFUL_DIALOG]: + [ + { + cond: "importSummaryHasDependencies", + target: "showDependenciesTab", + actions: ["fireChangeToDependenciesTab"], + }, + { + target: "showDescriptionTooltip", + actions: [ + "fireChangeToWorkflowTab", + "showTaskDescriptions", + ], + }, + ], + }, + }, + showDependenciesTab: { + tags: ["showDependenciesMessage"], + on: { + [DefinitionMachineEventTypes.NEXT_STEP_IMPORT_SUCCESSFUL_DIALOG]: + { + target: "showDescriptionTooltip", + actions: [ + "fireChangeToWorkflowTab", + "showTaskDescriptions", + ], + }, + }, + }, + showDescriptionTooltip: { + tags: ["showDescriptionTooltip"], + on: { + [DefinitionMachineEventTypes.NEXT_STEP_IMPORT_SUCCESSFUL_DIALOG]: + { + actions: ["showTaskDescriptions", "openLeftSidebar"], + target: "idle", + }, + }, + }, + }, + }, + }, + }, + agent: { + initial: "idle", + states: { + idle: { + on: { + [DefinitionMachineEventTypes.WORKFLOW_FROM_AGENT]: { + actions: [ + "persistWorkflowChanges", + "notifyFlowUpdatesFromEvent", + ], + target: "idle", + }, + }, + }, + }, + }, + }, + after: { + 5000: { + actions: ["cleanLocalCopyMessage"], + }, + }, + }, + errorFetchingWorkflow: { + on: { + [DefinitionMachineEventTypes.MESSAGE_RESET_EVT]: { + actions: ["resetMessage"], + target: "backToList", + }, + }, + }, + }, + }, + { + actions: actions as any, + services: { + saveMachine, + fetchSecretsEndEnvironmentsList, + persistCopyInLocalStorage, + removeCopyFromStorage, + refetchCurrentWorkflowVersionsService, + fetchInputSchema, + fetchForImportedTemplateImportSummary, + }, + guards: guards as any, + }, +); diff --git a/ui-next/src/pages/definition/state/services.ts b/ui-next/src/pages/definition/state/services.ts new file mode 100644 index 0000000000..76189a804b --- /dev/null +++ b/ui-next/src/pages/definition/state/services.ts @@ -0,0 +1,154 @@ +import fastDeepEqual from "fast-deep-equal"; +import { DefinitionMachineContext } from "pages/definition/state/types"; +import { + addLocalCopyTime, + extractKeyFromContext, +} from "pages/runWorkflow/runWorkflowUtils"; +import { fetchContextNonHook, fetchWithContext } from "plugins/fetch"; +import { queryClient } from "queryClient"; +import { WorkflowDef } from "types/WorkflowDef"; +import { + fetchCloudTemplatesPreferCached, + fetchWorkflowWithDependencies, + ImportSummary, +} from "utils/cloudTemplates"; +import { logger } from "utils/logger"; +import { getErrors } from "utils/utils"; +import { getEnvVariables } from "../commonService"; + +export { fetchCloudTemplatesPreferCached }; + +const fetchContext = fetchContextNonHook(); + +export const fetchForImportedTemplateImportSummary = async ( + context: DefinitionMachineContext, +): Promise => { + const { successfullyImportedWorkflowId: showImportSuccessfulDialog } = + context; + if (!showImportSuccessfulDialog) { + return null; + } + const templates = await fetchCloudTemplatesPreferCached(); + const importedTemplate = templates.cloudTemplates.find( + (t) => t.id === showImportSuccessfulDialog, + ); + if (importedTemplate != null) { + const importSummary = await fetchWorkflowWithDependencies(importedTemplate); + return importSummary; + } + return null; +}; + +export const persistCopyInLocalStorage = ( + context: DefinitionMachineContext, +): Promise => { + const { workflowChanges, currentWf } = context; + const isEqual = fastDeepEqual(currentWf, workflowChanges); + + if (!isEqual && context.workflowName != null) { + const wfKey = extractKeyFromContext({ + workflowName: context.workflowName, + currentVersion: context.currentVersion + ? Number(context.currentVersion) + : Number(context?.currentWf?.version), + isNewWorkflow: context.isNewWorkflow, + }); + + localStorage.setItem(wfKey, JSON.stringify(workflowChanges)); + addLocalCopyTime(wfKey); + + logger.log("Saved to local storage"); + + return Promise.resolve("Saved to local storage"); + } + + return Promise.resolve("Don't have any changes"); +}; + +export const fetchSecrets = async ({ + authHeaders: headers, +}: DefinitionMachineContext) => { + const url = `/secrets-v2`; + try { + const result = await queryClient.fetchQuery([fetchContext.stack, url], () => + fetchWithContext(url, fetchContext, { headers }), + ); + return result; + } catch { + return {}; + } +}; + +export const fetchInputSchema = async ({ + authHeaders: headers, + currentWf, +}: DefinitionMachineContext) => { + if (!currentWf?.inputSchema?.name || !currentWf?.inputSchema?.version) { + logger.warn("Missing input schema name or version in current workflow."); + return {}; + } + const schemaName = currentWf?.inputSchema?.name; + const schemaVersion = currentWf?.inputSchema?.version; + const url = `/schema/${schemaName}/${schemaVersion}`; + try { + const response = await queryClient.fetchQuery( + [fetchContext.stack, url], + () => fetchWithContext(url, fetchContext, { headers }), + ); + const properties = response?.data?.properties; + if (!properties || typeof properties !== "object") { + logger.warn("Schema response did not contain valid properties", response); + return {}; + } + return { schema: response?.data }; + } catch (error: any) { + logger.error("Failed to fetch input schema:", { + error, + schemaName, + schemaVersion, + }); + const errorMessage = (await getErrors(error))?.message; + + if (errorMessage) { + return Promise.reject({ message: errorMessage }); + } + + return {}; + } +}; + +export const fetchSecretsEndEnvironmentsList = async ( + context: DefinitionMachineContext, +) => { + const secrets = await fetchSecrets(context); + const envs = await getEnvVariables(context); + + return { + secrets, + envs: envs, + }; +}; + +export const refetchCurrentWorkflowVersionsService = async ({ + authHeaders: headers, + workflowName, +}: DefinitionMachineContext) => { + if (!workflowName) { + return {}; + } + + const url = `/metadata/workflow?includeShared=false&name=${encodeURIComponent( + workflowName, + )}`; + + try { + const result: WorkflowDef[] = await queryClient.fetchQuery( + [fetchContext.stack, url], + () => fetchWithContext(url, fetchContext, { headers }), + ); + const versions = result?.map((item) => item?.version) ?? []; + return { versions }; + } catch { + return {}; + } +}; diff --git a/ui-next/src/pages/definition/state/taskModifier/constants.ts b/ui-next/src/pages/definition/state/taskModifier/constants.ts new file mode 100644 index 0000000000..43a870888c --- /dev/null +++ b/ui-next/src/pages/definition/state/taskModifier/constants.ts @@ -0,0 +1,8 @@ +export const ADD_TASK_ABOVE = "ADD_TASK_ABOVE"; +export const DELETE_TASK = "DELETE_TASK"; +export const ADD_NEW_SWITCH_PATH = "ADD_NEW_SWITCH"; +export const ADD_TASK_BELOW = "ADD_TASK_BELOW"; +export const ADD_TASK = "ADD_TASK"; +export const REPLACE_TASK = "REPLACE_TASK"; +export const ADD_TASK_IN_DO_WHILE = "ADD_TASK_IN_DO_WHILE"; +export const REMOVE_BRANCH = "REMOVE_BRANCH"; diff --git a/ui-next/src/pages/definition/state/taskModifier/index.ts b/ui-next/src/pages/definition/state/taskModifier/index.ts new file mode 100644 index 0000000000..ea0b32192a --- /dev/null +++ b/ui-next/src/pages/definition/state/taskModifier/index.ts @@ -0,0 +1,2 @@ +export * from "./constants"; +export * from "./taskModifier"; diff --git a/ui-next/src/pages/definition/state/taskModifier/taskModifier.test.js b/ui-next/src/pages/definition/state/taskModifier/taskModifier.test.js new file mode 100644 index 0000000000..a1c7294d08 --- /dev/null +++ b/ui-next/src/pages/definition/state/taskModifier/taskModifier.test.js @@ -0,0 +1,581 @@ +import { + populationMinMax, + simpleDiagram, + simpleLoopSample, + switchExample, +} from "testData/diagramTests"; +import { + ADD_NEW_SWITCH_PATH, + ADD_TASK, + ADD_TASK_ABOVE, + DELETE_TASK, + REPLACE_TASK, +} from "./constants"; +import { + applyAddTask, + applyOperationArrayOnTasks, + findTaskModificationPath, + moveTask, + updateTaskReferenceName, +} from "./taskModifier"; + +describe("Task modifier", () => { + it("should find the cooresponding task array for a given task", () => { + const sampleCrumbs = [ + { parent: null, ref: "__start", refIdx: 0 }, + { parent: null, ref: "my_fork_join_ref", refIdx: 1 }, + { parent: "my_fork_join_ref", ref: "loop_2", refIdx: 0 }, + { parent: "loop_2", ref: "loop_2_task_iter", refIdx: 0 }, + { parent: "loop_2", ref: "loop_2_sv", refIdx: 0 }, + ]; + expect(findTaskModificationPath(sampleCrumbs, "loop_2_sv")).toEqual([ + { parent: null, ref: "my_fork_join_ref", refIdx: 1 }, + { parent: "my_fork_join_ref", ref: "loop_2", refIdx: 0 }, + { parent: "loop_2", ref: "loop_2_sv", refIdx: 0 }, + ]); + }); + + it("Should add an item to tasks array", () => { + const forkTaskIdxInTasks = 1; + const crumbs = [ + { parent: null, ref: "my_fork_join_ref", refIdx: forkTaskIdxInTasks }, + ]; + const taskToAdd = { + name: "sample_task_name_1", + taskReferenceName: "sample_task_name_a_ref", + type: "SIMPLE", + }; + + const modifiedTasks = applyOperationArrayOnTasks( + crumbs, + simpleLoopSample.tasks, + { type: ADD_TASK_ABOVE, payload: taskToAdd }, + ); + + expect(modifiedTasks).toEqual(expect.arrayContaining([])); + }); + + it("Should Add an item to a nested task within fork and loop", () => { + const forkTaskIdxWhereLoop2Is = 1; + const forkTaskIdxInTasks = 1; + const loopTaskIdxWithinForkTasks = 0; + const loop2InnerTaskIndexToApplyOperationOn = 0; + const taskToAdd = { + name: "sample_task_name_0", + taskReferenceName: "sample_task_name_0_ref", + type: "SIMPLE", + }; + const wfTasks = applyOperationArrayOnTasks( + [ + { parent: null, ref: "my_fork_join_ref", refIdx: forkTaskIdxInTasks }, + { + parent: "my_fork_join_ref", + ref: "loop_2", + refIdx: loopTaskIdxWithinForkTasks, + }, + { + parent: "loop_2", + ref: "loop_2_sv", + refIdx: loop2InnerTaskIndexToApplyOperationOn, + }, + ], + simpleLoopSample.tasks, + { type: ADD_TASK_ABOVE, payload: taskToAdd }, + ); + + const forkJoinTask = wfTasks[forkTaskIdxInTasks]; + const targetForkTasks = forkJoinTask.forkTasks[forkTaskIdxWhereLoop2Is]; + const loop2Task = targetForkTasks[loopTaskIdxWithinForkTasks]; + const loopingTasks = loop2Task.loopOver; + + expect(loopingTasks).toEqual(expect.arrayContaining([taskToAdd])); + }); + it("Should be able to delete a SIMPLE task", () => { + const simpleTaskCrumbsPath = [ + { + parent: null, + ref: "image_convert_resize_ref", + refIdx: 0, + }, + ]; + const wfTasks = applyOperationArrayOnTasks( + simpleTaskCrumbsPath, + simpleDiagram.tasks, + { type: DELETE_TASK }, + ); + expect( + wfTasks.map(({ taskReferenceName }) => taskReferenceName), + ).not.toEqual(expect.arrayContaining(["image_convert_resize_ref"])); + }); + + it("Should be able to delete a Fork Task. Deleting FORK should also delete JOIN", () => { + const forkTaskToDeleteModificationPath = [ + { + parent: null, + ref: "fork_ref", + refIdx: 2, + }, + ]; + const wfTasks = applyOperationArrayOnTasks( + forkTaskToDeleteModificationPath, + populationMinMax.tasks, + { type: DELETE_TASK }, + ); + expect(wfTasks.map(({ taskReferenceName }) => taskReferenceName)).toEqual([ + "get_population_data_ref", + ]); + }); + it("Should be able to add a SWITCH branch if task is type SWITCH", () => { + const taskContaingSwitch = [ + { + parent: null, + ref: "switch_task", + refIdx: 1, + }, + ]; + const wfTasks = applyOperationArrayOnTasks( + taskContaingSwitch, + switchExample.tasks, + { + type: ADD_NEW_SWITCH_PATH, + payload: { + branchName: "hasName", + }, + }, + ); + const [resultTask] = wfTasks.filter( + ({ taskReferenceName }) => taskReferenceName === "switch_task", + ); + expect(Object.keys(resultTask.decisionCases)).toEqual( + expect.arrayContaining(["hasName"]), + ); + // console.log(JSON.stringify(wfTasks, null, 2)); + }); + it("Should replace the task in the tree by the task sent in payload", () => { + const forkTaskToDeleteModificationPath = [ + { + parent: null, + ref: "get_population_data_ref", + refIdx: 0, + }, + ]; + + const wfTasks = applyOperationArrayOnTasks( + forkTaskToDeleteModificationPath, + populationMinMax.tasks, + { + type: REPLACE_TASK, + payload: { + name: "get_population_data", + taskReferenceName: "get_population_different_ref", + inputParameters: { + http_request: { + uri: "https://datausa.io/api/data?drilldowns=State&measures=Population&year=latest", + method: "GET", + }, + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + }, + ); + expect(wfTasks[0].taskReferenceName).toBe("get_population_different_ref"); + }); + it("Should prepend the task to the default branch in switch when adding a task", () => { + const taskContaingSwitch = [ + { + parent: null, + ref: "switch_task", + refIdx: 1, + onDecisionBranch: "defaultCase", + }, + ]; + const wfTasks = applyOperationArrayOnTasks( + taskContaingSwitch, + switchExample.tasks, + { + type: ADD_TASK, + payload: { + name: "prepended", + taskReferenceName: "prepended_task", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + }, + ); + const [resultTask] = wfTasks.filter( + ({ taskReferenceName }) => taskReferenceName === "switch_task", + ); + expect(resultTask.defaultCase.length).toBe(2); + expect( + resultTask.defaultCase.map(({ taskReferenceName }) => taskReferenceName), + ).toEqual(["prepended_task", "task_8_default"]); + }); + + it("Should be able to add the task to defaulCase even if defaultCase is empty", () => { + const taskContaingSwitch = [ + { + parent: null, + ref: "switch_task", + refIdx: 1, + onDecisionBranch: "defaultCase", + }, + ]; + const modifiedExampleEmptyDefaultCase = { + ...switchExample, + tasks: switchExample.tasks.map((task) => + task.type === "SWITCH" ? { ...task, defaultCase: [] } : task, + ), + }; + const wfTasks = applyOperationArrayOnTasks( + taskContaingSwitch, + modifiedExampleEmptyDefaultCase.tasks, + { + type: ADD_TASK, + payload: { + name: "prepended", + taskReferenceName: "prepended_task", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + }, + ); + const [resultTask] = wfTasks.filter( + ({ taskReferenceName }) => taskReferenceName === "switch_task", + ); + expect(resultTask.defaultCase.length).toBe(1); + expect( + resultTask.defaultCase.map(({ taskReferenceName }) => taskReferenceName), + ).toEqual(["prepended_task"]); + }); + it("Should add a dynamic fork task into an empty defaultCase of switch task", () => { + const taskArray = [ + { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "", + }, + type: "SWITCH", + decisionCases: {}, + defaultCase: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + }, + ]; + const idx = 0; + const payload = [ + { + name: "fork_join_dynamic", + taskReferenceName: "fork_join_dynamic_ref", + inputParameters: { + dynamicTasks: "", + dynamicTasksInput: "", + }, + type: "FORK_JOIN_DYNAMIC", + dynamicForkTasksParam: "dynamicTasks", + dynamicForkTasksInputParamName: "dynamicTasksInput", + startDelay: 0, + optional: false, + asyncComplete: false, + }, + { + name: "join", + taskReferenceName: "join_ref", + inputParameters: {}, + type: "JOIN", + joinOn: [], + optional: false, + asyncComplete: false, + }, + ]; + const crumbProps = { + parent: null, + ref: "switch_ref", + type: "SWITCH", + onDecisionBranch: "defaultCase", + }; + const result = applyAddTask(taskArray, idx, payload, crumbProps); + const expectedResult = [ + { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "", + }, + type: "SWITCH", + decisionCases: {}, + defaultCase: [ + { + name: "fork_join_dynamic", + taskReferenceName: "fork_join_dynamic_ref", + inputParameters: { + dynamicTasks: "", + dynamicTasksInput: "", + }, + type: "FORK_JOIN_DYNAMIC", + dynamicForkTasksParam: "dynamicTasks", + dynamicForkTasksInputParamName: "dynamicTasksInput", + startDelay: 0, + optional: false, + asyncComplete: false, + }, + { + name: "join", + taskReferenceName: "join_ref", + inputParameters: {}, + type: "JOIN", + joinOn: [], + optional: false, + asyncComplete: false, + }, + ], + evaluatorType: "value-param", + expression: "switchCaseValue", + }, + ]; + expect(result).toEqual(expectedResult); + }); +}); + +describe("moveTask", () => { + it("Should be able to drag a task inside an empty doWhile", () => { + const workflowChanges = { + name: "NewWorkflow_2qs7k", + description: "", + version: 1, + tasks: [ + { + name: "simple", + taskReferenceName: "simple_ref", + type: "SIMPLE", + }, + { + name: "do_while", + taskReferenceName: "do_while_ref", + inputParameters: {}, + type: "DO_WHILE", + startDelay: 0, + optional: false, + asyncComplete: false, + loopCondition: "", + evaluatorType: "value-param", + loopOver: [], + }, + ], + inputParameters: [], + outputParameters: {}, + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: false, + ownerEmail: "najeeb.thangal@orkes.io", + timeoutPolicy: "ALERT_ONLY", + timeoutSeconds: 0, + failureWorkflow: "", + }; + const sourceTask = { + name: "simple", + taskReferenceName: "simple_ref", + type: "SIMPLE", + }; + const sourceTaskCrumbs = [ + { + parent: null, + ref: "simple_ref", + refIdx: 0, + type: "SIMPLE", + }, + ]; + const targetTask = { + name: "do_while", + taskReferenceName: "do_while_ref", + inputParameters: {}, + type: "DO_WHILE", + startDelay: 0, + optional: false, + asyncComplete: false, + loopCondition: "", + evaluatorType: "value-param", + loopOver: [], + }; + const targetLocationCrumbs = [ + { + parent: null, + ref: "simple_ref", + refIdx: 0, + type: "SIMPLE", + }, + { + parent: null, + ref: "do_while_ref", + refIdx: 1, + type: "DO_WHILE", + }, + ]; + const position = "ADD_TASK_IN_DO_WHILE"; + const result = moveTask({ + workflow: workflowChanges, + source: { task: sourceTask, crumbs: sourceTaskCrumbs }, + target: { crumbs: targetLocationCrumbs, task: targetTask }, + position, + }); + const expectedResult = { + name: "NewWorkflow_2qs7k", + description: "", + version: 1, + tasks: [ + { + name: "do_while", + taskReferenceName: "do_while_ref", + inputParameters: {}, + type: "DO_WHILE", + startDelay: 0, + optional: false, + asyncComplete: false, + loopCondition: "", + evaluatorType: "value-param", + loopOver: [ + { + name: "simple", + taskReferenceName: "simple_ref", + type: "SIMPLE", + }, + ], + }, + ], + inputParameters: [], + outputParameters: {}, + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: false, + ownerEmail: "najeeb.thangal@orkes.io", + timeoutPolicy: "ALERT_ONLY", + timeoutSeconds: 0, + failureWorkflow: "", + }; + expect(result).toEqual(expectedResult); + }); +}); + +describe("updateTaskReferenceName", () => { + it("should update joinOn references in JOIN tasks", () => { + const tasks = [ + { + name: "fork", + taskReferenceName: "fork_ref", + inputParameters: {}, + type: "FORK_JOIN", + defaultCase: [], + forkTasks: [ + [ + { + name: "http_poll", + taskReferenceName: "http_poll_ref", + type: "HTTP_POLL", + inputParameters: { + http_request: { + uri: "https://orkes-api-tester.orkesconductor.com/api", + method: "GET", + accept: "application/json", + contentType: "application/json", + terminationCondition: + "(function(){ return $.output.response.body.randomInt > 10;})();", + pollingInterval: "60", + pollingStrategy: "FIXED", + encode: true, + }, + }, + }, + ], + [ + { + name: "event", + taskReferenceName: "event_x4f_ref", + type: "EVENT", + sink: "sqs:internal_event_name", + inputParameters: {}, + }, + ], + ], + }, + { + name: "join", + taskReferenceName: "join_ref", + inputParameters: {}, + type: "JOIN", + joinOn: ["event_x4f_ref", "http_poll_ref"], + optional: false, + asyncComplete: false, + }, + ]; + + const updated = updateTaskReferenceName( + tasks, + "http_poll_ref", + "http_poll_ref_updated", + ); + + expect(updated[1].joinOn).toContain("http_poll_ref_updated"); + expect(updated[1].joinOn).not.toContain("http_poll_ref"); + expect(updated[0]).toEqual(tasks[0]); + }); + + it("should not update joinOn if oldRef is not present", () => { + const tasks = [ + { + name: "join", + taskReferenceName: "join_ref", + type: "JOIN", + joinOn: ["ref2"], + inputParameters: {}, + }, + ]; + + const updated = updateTaskReferenceName(tasks, "ref1", "ref1_new"); + expect(updated[0].joinOn).toEqual(["ref2"]); + }); + + it("should not modify non-JOIN tasks", () => { + const tasks = [ + { + name: "simple", + taskReferenceName: "ref1", + type: "SIMPLE", + inputParameters: {}, + }, + ]; + + const updated = updateTaskReferenceName(tasks, "ref1", "ref1_new"); + expect(updated[0]).toEqual(tasks[0]); + }); + + it("should handle empty tasks array", () => { + expect(updateTaskReferenceName([], "ref1", "ref1_new")).toEqual([]); + }); +}); diff --git a/ui-next/src/pages/definition/state/taskModifier/taskModifier.ts b/ui-next/src/pages/definition/state/taskModifier/taskModifier.ts new file mode 100644 index 0000000000..ea876d465a --- /dev/null +++ b/ui-next/src/pages/definition/state/taskModifier/taskModifier.ts @@ -0,0 +1,712 @@ +import { + crumbsToTask, + removeTaskReferenceFromCrumbs, + START_TASK_FAKE_TASK_REFERENCE_NAME, +} from "components/flow/nodes/mapper"; +import _prop from "lodash/fp/prop"; +import _head from "lodash/head"; +import _isEmpty from "lodash/isEmpty"; +import _isNil from "lodash/isNil"; +import _last from "lodash/last"; +import _nth from "lodash/nth"; +import _omit from "lodash/omit"; +import _reverse from "lodash/reverse"; +import _tail from "lodash/tail"; +import { NodeData, PortData } from "reaflow"; +import { Crumb, TaskDef, TaskType, WorkflowDef } from "types"; +import { adjust, insert, logger, remove } from "utils"; +import { + ADD_NEW_SWITCH_PATH, + ADD_TASK, + ADD_TASK_ABOVE, + ADD_TASK_BELOW, + ADD_TASK_IN_DO_WHILE, + DELETE_TASK, + REMOVE_BRANCH, + REPLACE_TASK, +} from "./constants"; + +export const findTaskModificationPath = ( + crumbs: Crumb[], + taskReferenceName: string, +) => { + if (_isEmpty(crumbs)) return []; + const taskRefMap: Record = crumbs.reduce( + (acc, cur) => ({ ...acc, [cur.ref]: cur }), + {}, + ); + let currentTask: Crumb | undefined = taskRefMap[taskReferenceName]; + const taskPath = [currentTask]; + + while (!_isNil(currentTask?.parent)) { + currentTask = taskRefMap[currentTask.parent]; + taskPath.push(currentTask); + } + return _reverse(taskPath); +}; + +const applyDeleteOperation = (taskArray: TaskDef[], idx: number) => { + const task = taskArray[idx]; + const taskType = task.type; + switch (taskType) { + case TaskType.FORK_JOIN_DYNAMIC: + case TaskType.FORK_JOIN: { + return remove(idx, 2, taskArray); // Removes the FORK_JOIN and the JOIN task + } + case TaskType.JOIN: { + const previousIndex = idx - 1; + if (previousIndex >= 0) { + // If the previous task is a FORK_JOIN remove it as well + const previousTask = _nth(taskArray, previousIndex); + if ( + previousTask?.type === TaskType.FORK_JOIN || + previousTask?.type === TaskType.FORK_JOIN_DYNAMIC + ) + return remove(previousIndex, 2, taskArray); + } + return remove(idx, 1, taskArray); //If not just remove the task + } + default: { + return remove(idx, 1, taskArray); + } + } +}; + +const applyAddDecisionCase = ( + taskArray: TaskDef[], + idx: number, + { branchName }: { branchName?: string | number }, +) => { + const task = taskArray[idx]; + const { type, decisionCases } = task; + if (type === TaskType.SWITCH || type === TaskType.DECISION) { + return adjust( + idx, + () => ({ + ...task, + decisionCases: { ...decisionCases, [branchName!]: [] }, + }), + taskArray, + ); + } else if (type === TaskType.FORK_JOIN) { + const result = adjust( + idx, + () => ({ + ...task, + forkTasks: (task?.forkTasks ?? []).concat([[]]), + }), + taskArray, + ); + return result; + } + + logger.warn("Got wrong task as reference type is ", type); + + return taskArray; +}; + +// TODO Add unit test for this +const applyAddTaskInDoWhile = ( + taskArray: TaskDef[], + idx: number, + taskToAdd: TaskDef, +) => { + const task = taskArray[idx]; + const { type, loopOver = [] } = task; + if (type === TaskType.DO_WHILE) { + return adjust( + idx, + () => ({ + ...task, + loopOver: loopOver.concat(taskToAdd), + }), + taskArray, + ); + } + logger.warn("Got wrong task as reference type expected DO_WHILE ", type); + + return taskArray; +}; + +export const applyAddTask = ( + taskArray: TaskDef[], + idx: number, + payload: Record | TaskDef[], + crumbProps: { onDecisionBranch?: string; forkIdx?: number }, +) => { + const task = taskArray[idx]; + + if (task.type === TaskType.SWITCH || task.type === TaskType.DECISION) { + const { onDecisionBranch } = crumbProps; + return adjust( + idx, + () => + ({ + ...task!, + ...(onDecisionBranch === "defaultCase" + ? { + defaultCase: Array.isArray(payload) + ? [...payload, ...(task?.defaultCase || [])] + : [payload, ...(task?.defaultCase || [])], + } + : { + decisionCases: { + ...task.decisionCases, + [onDecisionBranch!]: Array.isArray(payload) + ? [ + ...payload, + ...(_prop(onDecisionBranch!, task?.decisionCases) || + []), + ] + : [ + payload, + ...(_prop(onDecisionBranch!, task?.decisionCases) || + []), + ], + }, + }), + }) as TaskDef, + taskArray, + ); + } else if (task.type === TaskType.FORK_JOIN) { + const { forkIdx } = crumbProps; + const tasksToAppend = Array.isArray(payload) ? [...payload] : [payload]; + + const forkTasks = adjust( + forkIdx!, + () => [...tasksToAppend, ...(_nth(task?.forkTasks, forkIdx) || [])], + task?.forkTasks || [], + ); + + return adjust( + idx, + () => ({ + ...task, + forkTasks, + }), + taskArray, + ); + } + + logger.warn("Got wrong task as reference type is ", task.type); + return []; +}; + +const applyRemoveBranch = ( + taskArray: TaskDef[], + idx: number, + { branchName }: { branchName?: string | number }, +) => { + const task = taskArray[idx]; + const { type, decisionCases } = task; + if (type === TaskType.SWITCH || type === TaskType.DECISION) { + const taskModification = + branchName === "defaultCase" + ? { defaultCase: [] } + : { + decisionCases: _omit(decisionCases, branchName!), + }; + + return adjust( + idx, + () => ({ + ...task, + ...taskModification, + }), + taskArray, + ); + } else if (type === TaskType.FORK_JOIN) { + const tasksWithoutForkBranch = adjust( + idx, + () => ({ + ...task, + forkTasks: remove( + branchName! as number, + 1, + task.forkTasks as TaskDef[][], + ), + }), + taskArray, + ); + + const nextTaskIndex = idx + 1; + const maybeNextTask = _nth(tasksWithoutForkBranch, nextTaskIndex) as + | TaskDef + | undefined; + + // Remove join on items in JOIN task + if (maybeNextTask != null && maybeNextTask?.type === TaskType.JOIN) { + const maybeLastTaskInBranch = _last( + _nth(task.forkTasks, branchName as number), + ); + + const originalJoinOn = maybeNextTask?.joinOn || []; + const joinOn = + maybeLastTaskInBranch == null + ? originalJoinOn + : originalJoinOn.filter( + (joinTask) => + joinTask !== maybeLastTaskInBranch?.taskReferenceName, + ); + + return adjust( + nextTaskIndex, + () => ({ + ...maybeNextTask, + joinOn, + }), + tasksWithoutForkBranch, + ); + } + + return tasksWithoutForkBranch; + } + + logger.warn("Got wrong task as reference type is ", type); + + return taskArray; +}; + +const applySingleOperationOnTaskArray = ( + taskArray: TaskDef[], + operation: { + type: string; + parameters: { + idx: number; + payload: { branchName?: string | number; crumb?: Crumb }; + }; + }, +): TaskDef[] => { + const { + type, + parameters: { idx, payload, ...otherCrumbProps }, + } = operation; + + switch (type) { + case ADD_TASK_ABOVE: { + return insert(idx, payload as TaskDef, taskArray); + } + case ADD_TASK_BELOW: { + const taskIdxInc = idx + 1; + return insert(taskIdxInc, payload as TaskDef, taskArray); + } + case DELETE_TASK: { + return applyDeleteOperation(taskArray, idx); + } + case ADD_NEW_SWITCH_PATH: { + return applyAddDecisionCase(taskArray, idx, payload); + } + case ADD_TASK: { + return applyAddTask(taskArray, idx, payload, otherCrumbProps); + } + case REPLACE_TASK: { + return adjust(idx, () => payload as TaskDef, taskArray); + } + case ADD_TASK_IN_DO_WHILE: { + return applyAddTaskInDoWhile(taskArray, idx, payload as TaskDef); + } + case REMOVE_BRANCH: { + return applyRemoveBranch(taskArray, idx, payload); + } + default: { + return taskArray; + } + } +}; + +type OperationType = { + payload: any; + type: string; +}; + +export const applyOperationArrayOnTasks = ( + fwCrumb: Crumb[], + tasks: TaskDef[], + operation: OperationType = { payload: {}, type: "" }, +): TaskDef[] => { + if (_isEmpty(fwCrumb) || _isNil(_head(fwCrumb))) { + return applySingleOperationOnTaskArray(tasks, { + ...operation, + parameters: { + idx: 0, + payload: operation.payload || {}, + }, + }); + } + const { refIdx, ...otherCrumbProps } = _head(fwCrumb) as Crumb; + const restCrumbs = _tail(fwCrumb); + + const isLastCrumb = restCrumbs.length === 0; + + if (isLastCrumb) { + return applySingleOperationOnTaskArray(tasks, { + ...operation, + + parameters: { + payload: operation.payload || {}, + idx: refIdx, + ...otherCrumbProps, + }, + }); + } + + const currentTask = tasks[refIdx]; + if (currentTask.type === TaskType.FORK_JOIN) { + const { forkTasks = [] } = currentTask; + const joinTask = tasks[refIdx + 1]; + const { ref: targetInnerForkTaskRef, refIdx: targetInnerForkTaskRefIdx } = + _head(restCrumbs) as Crumb; + const innerForkTaskReference = forkTasks.findIndex((innerTasks) => { + return ( + innerTasks[targetInnerForkTaskRefIdx]?.taskReferenceName === + targetInnerForkTaskRef + ); + }); + if (innerForkTaskReference === -1) { + throw Error("Task not found inconsistent state"); + } + // Cleanup join task joinOn if the task is deleted + if ( + joinTask?.type === TaskType.JOIN && + operation.type === DELETE_TASK && + joinTask.joinOn.includes(targetInnerForkTaskRef) + ) { + joinTask.joinOn = joinTask.joinOn.filter( + (joinOn) => joinOn !== targetInnerForkTaskRef, + ); + } + const updatedForkTasks = applyOperationArrayOnTasks( + restCrumbs, + forkTasks[innerForkTaskReference], + operation, + ); + return adjust( + refIdx, + () => ({ + ...currentTask, + forkTasks: adjust( + innerForkTaskReference, + () => updatedForkTasks, + forkTasks, + ), + }), + tasks, + ); + } else if (currentTask.type === TaskType.DO_WHILE) { + const { loopOver = [] } = currentTask; + const updatedLoopOver = applyOperationArrayOnTasks( + restCrumbs, + loopOver, + operation, + ); + + return adjust( + refIdx, + () => ({ ...currentTask, loopOver: updatedLoopOver }), + tasks, + ); + } else if ( + currentTask.type === TaskType.SWITCH || + currentTask.type === TaskType.DECISION + ) { + const { decisionBranch } = _head(restCrumbs) as Crumb; + const { decisionCases = {}, defaultCase } = currentTask; + const isDefault = decisionBranch === "defaultCase"; + + const decisionCaseTasksAfected = isDefault + ? defaultCase + : decisionCases[decisionBranch!]; + + const updatedDecisionCase = applyOperationArrayOnTasks( + restCrumbs, + decisionCaseTasksAfected || [], + operation, + ); + const updated = isDefault + ? { defaultCase: updatedDecisionCase } + : { + decisionCases: { + ...decisionCases, + [decisionBranch as string]: updatedDecisionCase, + }, + }; + + return adjust( + refIdx, + () => ({ + ...currentTask, + ...updated, + }), + tasks, + ); + } + + return tasks; +}; + +export function updateTaskReferenceName( + tasks: TaskDef[], + oldRef: string, + newRef: string, +): TaskDef[] { + return tasks.map((task) => + task.type === TaskType.JOIN && Array.isArray(task.joinOn) + ? { + ...task, + joinOn: task.joinOn.map((ref) => (ref === oldRef ? newRef : ref)), + } + : task, + ); +} + +type PerformOperationArgs = { + workflow?: Partial; + crumbs: Crumb[]; + taskDef: TaskDef; + operation: OperationType; +}; + +export const performOperation = ({ + workflow, + crumbs, + taskDef: { taskReferenceName }, + operation, +}: PerformOperationArgs) => { + if (!workflow) { + throw new Error("No context workflow provided"); + } + + return { + ...workflow, + tasks: applyOperationArrayOnTasks( + findTaskModificationPath(crumbs, taskReferenceName), + workflow?.tasks || [], + operation, + ), + }; +}; +type TaskAndCrumbs = { task: TaskDef; crumbs: Crumb[] }; + +type MoveTaskArgs = { + workflow?: Partial; + source: TaskAndCrumbs; + target: TaskAndCrumbs; + position: string; +}; + +export const moveTask = ({ + workflow, + source: { task: originTaskToMove, crumbs: originCrumbsToMove }, + target: { task: belowDestinationTask, crumbs: belowDestinationTaskCrumbs }, + position, +}: MoveTaskArgs) => { + if (!workflow) { + throw new Error("No context workflow provided"); + } + + const PAYLOAD_MODIFICATION_OPERATION = + belowDestinationTask.type === TaskType.TERMINAL && + belowDestinationTask.taskReferenceName === + START_TASK_FAKE_TASK_REFERENCE_NAME + ? ADD_TASK_ABOVE + : position; + + if ( + [TaskType.FORK_JOIN, TaskType.FORK_JOIN_DYNAMIC].includes( + originTaskToMove.type, + ) + ) { + const maybeLastCrumb = _last(originCrumbsToMove); + if (maybeLastCrumb?.refIdx != null) { + const pseudoJoinCrumbs = [ + ...originCrumbsToMove, + { + ...maybeLastCrumb, + refIdx: maybeLastCrumb?.refIdx + 1, // Kind of dangerous operation but we are removing the task after the fork + ref: "fake_join", + }, + ]; + + // original join task + const maybeJoinTask = crumbsToTask( + pseudoJoinCrumbs, + workflow.tasks || [], + ); + + if (maybeJoinTask?.type === TaskType.JOIN) { + // Lets assert is a join + // removes the fork and the join + const removeTaskResult = performOperation({ + workflow, + crumbs: originCrumbsToMove, + taskDef: originTaskToMove, + operation: { + type: DELETE_TASK, + payload: {}, + }, + }); + + // remove crumb for fork + let updatedCrumbs = removeTaskReferenceFromCrumbs( + belowDestinationTaskCrumbs, + originTaskToMove.taskReferenceName, + ); + + //remove crumb for join + updatedCrumbs = removeTaskReferenceFromCrumbs( + updatedCrumbs, + maybeJoinTask.taskReferenceName, + ); + // add original fork and join + const addTaskResult = performOperation({ + workflow: removeTaskResult, + crumbs: updatedCrumbs, + taskDef: belowDestinationTask, + operation: { + type: PAYLOAD_MODIFICATION_OPERATION, + payload: [originTaskToMove, maybeJoinTask], + }, + }); + + return addTaskResult; + } + logger.warn("Undefined behavior, join not found"); + } + } + + const removeTaskResult = performOperation({ + workflow, + crumbs: originCrumbsToMove, + taskDef: originTaskToMove, + operation: { + type: DELETE_TASK, + payload: {}, + }, + }); + + const updatedCrumbs = removeTaskReferenceFromCrumbs( + belowDestinationTaskCrumbs, + originTaskToMove.taskReferenceName, + ); + + const addTaskResult = performOperation({ + workflow: removeTaskResult, + crumbs: updatedCrumbs, + taskDef: belowDestinationTask, + operation: { + type: PAYLOAD_MODIFICATION_OPERATION, + payload: originTaskToMove, + }, + }); + return addTaskResult; +}; + +// TODO Change this to a reducer + +const keyIdentifier = "[key="; //This should not be here extract to constant file + +const portIdToDecisionBranch = (portId: string) => { + const keyIdx = portId.indexOf(keyIdentifier); + const endIdx = portId.indexOf("]"); + if (keyIdx === -1) { + throw new Error("Port id is not a decision branch"); + } + + return portId.substring(keyIdx + keyIdentifier.length, endIdx); +}; +export const buildDataForRemoveBranchOperation = ({ + port, + node, +}: { + port: PortData; + node: NodeData; +}) => { + const branchName = portIdToDecisionBranch(port.id); + return { + ...node.data, + branchName, + }; +}; + +export const buildDataForOperation = ( + port: PortData & { properties: { id?: string; side: string } }, + { data, ports = [] }: NodeData, +) => { + const portId = port?.properties?.id; + const { task, crumbs } = data; + if (task.type === TaskType.TERMINAL) { + return { + data: { + ...data, + action: ADD_TASK_ABOVE, + }, + }; + } else if ( + task.type === TaskType.FORK_JOIN && + port?.properties?.side === "SOUTH" + ) { + const forkIdx = ports.findIndex(({ id }: { id: string }) => portId === id); + return { + data: { + ...data, + crumbs: adjust( + crumbs.length - 1, + () => ({ + ...(_last(crumbs) || {}), + forkIdx, + }), + crumbs, + ), + action: ADD_TASK, + }, + }; + } else if (task.type === TaskType.SWITCH) { + if (port?.properties?.side === "SOUTH") { + const decisionBranch = portIdToDecisionBranch(portId!); + return { + data: { + ...data, + crumbs: adjust( + crumbs.length - 1, + () => ({ + ...(_last(crumbs) || {}), + onDecisionBranch: decisionBranch, + }), + crumbs, + ), + action: ADD_TASK, + }, + }; + } else if (port?.properties?.side === "INNER") { + // Special case this port does not exist but references a button + return { + data: { + ...data, + action: ADD_TASK_BELOW, + }, + }; + } + } else if ( + task.type === TaskType.DO_WHILE && + data.action === ADD_TASK_IN_DO_WHILE + ) { + return { data }; + } + return { + data: { + ...data, + action: + port.properties.side === "SOUTH" ? ADD_TASK_BELOW : ADD_TASK_ABOVE, + }, + }; +}; + +export const positionIdentifier = (position: string) => { + if (position === "BELOW") { + return ADD_TASK_BELOW; + } + if (position === "ADD_TASK_IN_DO_WHILE") { + return ADD_TASK_IN_DO_WHILE; + } + return ADD_TASK_ABOVE; +}; diff --git a/ui-next/src/pages/definition/state/types.ts b/ui-next/src/pages/definition/state/types.ts new file mode 100644 index 0000000000..dff556aacc --- /dev/null +++ b/ui-next/src/pages/definition/state/types.ts @@ -0,0 +1,351 @@ +import { ActorRef, DoneInvokeEvent } from "xstate"; +import { + ErrorInspectorMachineEvents, + WorkflowWithNoErrorsEvent, +} from "../errorInspector/state"; +import { UseLocalCopyChangesEvent } from "../ConfirmLocalCopyDialog/state"; +import { + AuthHeaders, + Crumb, + TaskDef, + WorkflowDef, + WorkflowMetadataI, + User, +} from "types"; +import { FlowEvents } from "components/flow/state"; +import { CodeTextReference } from "../EditorPanel/CodeEditorTab/state"; +import { + SavedCancelledEvent, + SavedSuccessfulEvent, +} from "../confirmSave/state"; +import { ImportSummary } from "utils/cloudTemplates"; + +type ImportantMessage = { + text?: string; + severity?: string; +}; + +export type OperationContext = { + task: TaskDef; + crumbs: Crumb[]; + action: string; // Not really a string +}; + +export enum LeftPaneTabs { + WORKFLOW_TAB = 0, + TASK_TAB = 1, + CODE_TAB = 2, + RUN_TAB = 3, + DEPENDENCIES_TAB = 4, +} + +export type RunWorkflowFields = { + input?: string; + correlationId?: string; + taskToDomain?: string; + idempotencyKey?: string; + idempotencyStrategy?: any; +}; + +export enum DefinitionMachineEventTypes { + UPDATE_ATTRIBS_EVT = "updateAttributes", + SAVE_EVT = "save", + RESET_EVT = "reset", + DELETE_EVT = "delete", + CHANGE_VERSION_EVT = "changeVersion", + ASSIGN_MESSAGE_EVT = "assignMessage", + MESSAGE_RESET_EVT = "messageReset", + RESET_CONFIRM_EVT = "resetConfirm", + CANCEL_EVENT_EVT = "cancel", + DELETE_CONFIRM_EVT = "deleteConfirm", + CHANGE_TAB_EVT = "changeTab", + PERFORM_OPERATION_EVT = "performOperation", + REPLACE_TASK_EVT = "replaceTask", + DEBOUNCE_REPLACE_TASK_EVT = "debounceReplaceTask", + REMOVE_TASK_EVT = "removeTask", + ADD_NEW_SWITCH_PATH_EVT = "addNewSwitchPathToTask", + UPDATE_WF_METADATA_EVT = "updateWorkflowMetadata", + REMOVE_BRANCH_EVT = "removeBranch", + TOGGLE_META_BAR_EDIT_MODE_EVT = "toggleMetaBarEditMode", + FLOW_FINISHED_RENDERING = "FLOW_FINISHED_RENDERING", + DOWNLOAD_FILE_REQUEST = "DOWNLOAD_FILE_REQUEST", + CONFIRM_LAST_FORK_REMOVAL = "CONFIRM_LAST_FORK_REMOVAL", + MOVE_TASK_EVT = "MOVE_TASK_EVT", + HANDLE_LEFT_PANEL_EXPANDED = "HANDLE_LEFT_PANEL_EXPANDED", + HANDLE_SAVE_AND_RUN = "HANDLE_SAVE_AND_RUN", + REDIRECT_TO_EXECUTION_PAGE = "REDIRECT_TO_EXECUTION_PAGE", + HANDLE_SAVE_AND_CREATE_NEW = "HANDLE_SAVE_AND_CREATE_NEW", + EXECUTE_WF = "EXECUTE_WF", + NEXT_STEP_IMPORT_SUCCESSFUL_DIALOG = "NEXT_STEP_IMPORT_SUCCESSFUL_DIALOG", + DISMISS_IMPORT_SUCCESSFUL_DIALOG = "DISMISS_IMPORT_SUCCESSFUL_DIALOG", + SYNC_RUN_CONTEXT_AND_CHANGE_TAB = "SYNC_RUN_CONTEXT_AND_CHANGE_TAB", + COLLAPSE_SIDEBAR_AND_RIGHT_PANEL = "COLLAPSE_SIDEBAR_AND_RIGHT_PANEL", + WORKFLOW_FROM_AGENT = "WORKFLOW_FROM_AGENT", + TOGGLE_AGENT_EXPANDED = "TOGGLE_AGENT_EXPANDED", +} + +export const DONT_SHOW_IMPORT_SUCCESSFUL_DIALOG_TUTORIAL_AGAIN = + "dontShowImportSuccessfulDialogTutorialAgain:2"; + +export type PerformedOperation = { + payload: Partial | Partial[]; +}; + +export type ErrorOnInvokeEvent = DoneInvokeEvent<{ + originalError: { status: number }; + errorDetails: { message: string }; +}>; + +export type UpdateAttributesEvent = { + type: DefinitionMachineEventTypes.UPDATE_ATTRIBS_EVT; + isNewWorkflow: boolean; + workflowName: string; + workflowVersions?: number[]; + currentVersion?: string; + workflowTemplateId?: string; +}; + +export type ChangeVersionEvent = { + type: DefinitionMachineEventTypes.CHANGE_VERSION_EVT; + version: string; +}; + +export type ChangeTabEvent = { + type: DefinitionMachineEventTypes.CHANGE_TAB_EVT; + tab: LeftPaneTabs; +}; + +export type PerformOperationEvent = { + type: DefinitionMachineEventTypes.PERFORM_OPERATION_EVT; + data: OperationContext; + operation: PerformedOperation; +}; + +export type ReplaceTaskEvent = { + type: DefinitionMachineEventTypes.REPLACE_TASK_EVT; + task: TaskDef; + crumbs: Crumb[]; + newTask: TaskDef; +}; + +export type RemoveTaskEvent = { + type: DefinitionMachineEventTypes.REMOVE_TASK_EVT; + task: TaskDef; + crumbs: Crumb[]; +}; + +export type AddNewSwitchTaskEvent = { + type: DefinitionMachineEventTypes.ADD_NEW_SWITCH_PATH_EVT; + task: TaskDef; + crumbs: Crumb[]; +}; + +type RemovalOperationPayload = { + task: TaskDef; + crumbs: Crumb[]; + branchName: string; +}; + +export type RemoveBranchFromTaskEvent = { + type: DefinitionMachineEventTypes.REMOVE_BRANCH_EVT; +} & RemovalOperationPayload; + +export type UpdateWorkflowMetadataEvent = { + type: DefinitionMachineEventTypes.UPDATE_WF_METADATA_EVT; + workflowMetadata: Partial; +}; + +export type DownloadFileEvent = { + type: DefinitionMachineEventTypes.DOWNLOAD_FILE_REQUEST; +}; + +export type SaveRequestEvent = { + type: DefinitionMachineEventTypes.SAVE_EVT; +}; + +export type SaveAndCreateNewRequestEvent = { + type: DefinitionMachineEventTypes.SAVE_EVT; + originalEvent: DefinitionMachineEventTypes; +}; + +export type HandleSaveAndRunEvent = { + type: DefinitionMachineEventTypes.HANDLE_SAVE_AND_RUN; +}; + +export type HandleSaveAndCreateNewEvent = { + type: DefinitionMachineEventTypes.HANDLE_SAVE_AND_CREATE_NEW; +}; + +export type SaveAsNewVersionRequestEvent = { + type: DefinitionMachineEventTypes.SAVE_EVT; + isNewVersion: boolean; +}; + +export type SaveAndRunRequestEvent = { + type: DefinitionMachineEventTypes.SAVE_EVT; + isSaveAndRun: boolean; +}; + +export type ResetRequestEvent = { + type: DefinitionMachineEventTypes.RESET_EVT; +}; + +export type ResetConfirmEvent = { + type: DefinitionMachineEventTypes.RESET_CONFIRM_EVT; +}; + +export type DeleteConfirmEvent = { + type: DefinitionMachineEventTypes.DELETE_CONFIRM_EVT; +}; + +export type CancelEvent = { + type: DefinitionMachineEventTypes.CANCEL_EVENT_EVT; +}; + +export type DeleteRequestEvent = { + type: DefinitionMachineEventTypes.DELETE_EVT; +}; + +export type ConfirmLastForkTaskRemoval = { + type: DefinitionMachineEventTypes.CONFIRM_LAST_FORK_REMOVAL; +}; + +export type CollapseSidebarAndRightPanel = { + type: DefinitionMachineEventTypes.COLLAPSE_SIDEBAR_AND_RIGHT_PANEL; +}; + +export type MoveTaskEvent = { + type: DefinitionMachineEventTypes.MOVE_TASK_EVT; + sourceTask: TaskDef; + sourceTaskCrumbs: Crumb[]; + targetLocationCrumbs: Crumb[]; + targetTask: TaskDef; + position: "ABOVE" | "BELOW" | "ADD_TASK_IN_DO_WHILE"; +}; + +export type DoneInvokeLocalStorageMachineEvent = { + type: "done.invoke.localCopyMachine"; + data: { workflow?: Partial; isLocalStorageEmpty: boolean }; +}; + +export type HandleLeftPanelExpandedEvent = { + type: DefinitionMachineEventTypes.HANDLE_LEFT_PANEL_EXPANDED; + onSelectNode: boolean; +}; + +export type MessageResetEvent = { + type: DefinitionMachineEventTypes.MESSAGE_RESET_EVT; +}; + +export type RedirectToExecutionPageEvent = { + type: DefinitionMachineEventTypes.REDIRECT_TO_EXECUTION_PAGE; + executionId: string; +}; + +export type ExecuteWfEvent = { + type: DefinitionMachineEventTypes.EXECUTE_WF; +}; + +export type NextStepImportSuccessfulDialogEvent = { + type: DefinitionMachineEventTypes.NEXT_STEP_IMPORT_SUCCESSFUL_DIALOG; +}; + +export type DismissImportSuccessfulDialogEvent = { + type: DefinitionMachineEventTypes.DISMISS_IMPORT_SUCCESSFUL_DIALOG; +}; + +export type SyncRunContextAndChangeTabEvent = { + type: DefinitionMachineEventTypes.SYNC_RUN_CONTEXT_AND_CHANGE_TAB; + data: { + originalEvent: ChangeTabEvent; + runMachineContext: RunWorkflowFields; + }; +}; + +export type WorkflowFromAgentEvent = { + type: DefinitionMachineEventTypes.WORKFLOW_FROM_AGENT; + workflow: Partial; +}; + +export type ToggleAgentExpandedEvent = { + type: DefinitionMachineEventTypes.TOGGLE_AGENT_EXPANDED; + expanded?: boolean; // Optional: if provided, sets to that value; otherwise toggles +}; + +export type WorkflowDefinitionEvents = + | ConfirmLastForkTaskRemoval + | UpdateAttributesEvent + | ErrorOnInvokeEvent + | ChangeTabEvent + | PerformOperationEvent + | ReplaceTaskEvent + | RemoveTaskEvent + | AddNewSwitchTaskEvent + | RemoveBranchFromTaskEvent + | UpdateWorkflowMetadataEvent + | WorkflowWithNoErrorsEvent + | DownloadFileEvent + | SavedSuccessfulEvent + | SavedCancelledEvent + | SaveRequestEvent + | SaveAndCreateNewRequestEvent + | SaveAsNewVersionRequestEvent + | ResetRequestEvent + | DeleteRequestEvent + | ResetConfirmEvent + | DeleteConfirmEvent + | CancelEvent + | UseLocalCopyChangesEvent + | DoneInvokeLocalStorageMachineEvent + | MoveTaskEvent + | ChangeVersionEvent + | HandleLeftPanelExpandedEvent + | MessageResetEvent + | HandleSaveAndRunEvent + | HandleSaveAndCreateNewEvent + | SaveAndRunRequestEvent + | RedirectToExecutionPageEvent + | ExecuteWfEvent + | NextStepImportSuccessfulDialogEvent + | DismissImportSuccessfulDialogEvent + | SyncRunContextAndChangeTabEvent + | CollapseSidebarAndRightPanel + | WorkflowFromAgentEvent + | ToggleAgentExpandedEvent; +export interface DefinitionMachineContext { + currentWf?: Partial; + workflowChanges?: Partial; + currentUserInfo?: User; + isNewWorkflow: boolean; + workflowName?: string; + currentVersion?: string; + workflowVersions: number[]; + selectedTaskCrumbs: Crumb[]; + authHeaders: AuthHeaders; // This should be in auth actor + message: ImportantMessage; + openedTab: LeftPaneTabs; + previousTab: LeftPaneTabs; + lastPerformedOperation?: PerformedOperation; + errorInspectorMachine?: ActorRef; + downloadFileObj?: { + data: Partial; + fileName: string; + type: `application/json`; + }; + lastRemovalOperation?: RemovalOperationPayload; + flowMachine?: ActorRef; + workflowTemplateId?: string; + localCopyMessage?: string; + collapseWorkflowList?: string[]; + codeTextReference?: CodeTextReference; + isNewVersion?: boolean; + secrets?: Record[]; + envs?: Record; + initialSelectedTaskReferenceName?: string; // This is to dispatch to the flow machine + workflowDefaultRunParam?: Record; + saveSourceEventType?: DefinitionMachineEventTypes; // This is to save the event source in context + successfullyImportedWorkflowId?: string; + importSummary?: ImportSummary; + runTabFormState?: RunWorkflowFields; + isAgentExpanded?: boolean; +} diff --git a/ui-next/src/pages/definition/state/useGetVariablesForSelectedTasks.ts b/ui-next/src/pages/definition/state/useGetVariablesForSelectedTasks.ts new file mode 100644 index 0000000000..2d8ac7a5a7 --- /dev/null +++ b/ui-next/src/pages/definition/state/useGetVariablesForSelectedTasks.ts @@ -0,0 +1,31 @@ +import { useSelector } from "@xstate/react"; +import { useMemo } from "react"; +import { crumbsToTaskSteps } from "components/flow/nodes"; +import _initial from "lodash/initial"; +import { extractVariablesFromTask } from "../helpers"; +import { ActorRef } from "xstate"; +import { WorkflowDefinitionEvents } from "./types"; + +export const useGetVariablesForSelectedTasks = ( + workflowDefinitionActor: ActorRef | undefined, +) => { + const selectedTaskCrumbs = useSelector( + workflowDefinitionActor!, + (state) => state.context.selectedTaskCrumbs, + ); + + const editorTasks = useSelector( + workflowDefinitionActor!, + (state) => state.context.workflowChanges.tasks, + ); + + const tasksInCrumbBranch = useMemo(() => { + if (editorTasks.length > 0) { + return _initial(crumbsToTaskSteps(selectedTaskCrumbs, editorTasks)); + } + return []; + }, [editorTasks, selectedTaskCrumbs]); + + const variableInputs = extractVariablesFromTask(tasksInCrumbBranch); + return variableInputs; +}; diff --git a/ui-next/src/pages/definition/state/useMadeChanges.ts b/ui-next/src/pages/definition/state/useMadeChanges.ts new file mode 100644 index 0000000000..1f644f0287 --- /dev/null +++ b/ui-next/src/pages/definition/state/useMadeChanges.ts @@ -0,0 +1,33 @@ +import { ActorRef } from "xstate"; +import { useMemo } from "react"; +import { WorkflowDefinitionEvents } from "./types"; +import { useSelector } from "@xstate/react"; +import fastDeepEqual from "fast-deep-equal"; +/* +Use this hook as low as the state tree can go. it is subscribed to workflowChanges +*/ +export const useWorkflowChanges = ( + service: ActorRef, +) => { + const isNewWorkflow = useSelector( + service, + (state) => state.context.isNewWorkflow, + ); + const currentWf = useSelector(service, (state) => state.context.currentWf); + + const workflowChanges = useSelector( + service, + (state) => state.context.workflowChanges, + ); + + const madeChanges = useMemo(() => { + return isNewWorkflow ? true : !fastDeepEqual(workflowChanges, currentWf); + }, [workflowChanges, currentWf, isNewWorkflow]); + + return { + isNewWorkflow, + currentWf, + workflowChanges, + madeChanges, + }; +}; diff --git a/ui-next/src/pages/definition/state/usePanelChanges.ts b/ui-next/src/pages/definition/state/usePanelChanges.ts new file mode 100644 index 0000000000..0d203c5e05 --- /dev/null +++ b/ui-next/src/pages/definition/state/usePanelChanges.ts @@ -0,0 +1,22 @@ +import { useSelector } from "@xstate/react"; +import { ActorRef } from "xstate"; + +import { + DefinitionMachineEventTypes, + WorkflowDefinitionEvents, +} from "pages/definition/state/types"; + +export const usePanelChanges = (actor: ActorRef) => { + const setLeftPanelExpanded = () => { + actor.send({ + type: DefinitionMachineEventTypes.HANDLE_LEFT_PANEL_EXPANDED, + onSelectNode: false, + }); + }; + + const leftPanelExpanded = useSelector(actor, (state) => + state.matches("ready.rightPanel.closed"), + ); + + return { leftPanelExpanded, setLeftPanelExpanded }; +}; diff --git a/ui-next/src/pages/definition/state/usePerformOperationOnDefintion.ts b/ui-next/src/pages/definition/state/usePerformOperationOnDefintion.ts new file mode 100644 index 0000000000..484c7b432c --- /dev/null +++ b/ui-next/src/pages/definition/state/usePerformOperationOnDefintion.ts @@ -0,0 +1,71 @@ +import { TaskDef, Crumb } from "types"; +import { ActorRef } from "xstate"; +import { + WorkflowDefinitionEvents, + DefinitionMachineEventTypes, + OperationContext, + PerformedOperation, +} from "./types"; + +export type TaskAndCrumbs = { + task: TaskDef; + crumbs: Crumb[]; +}; + +export const usePerformOperationOnDefinition = ( + service: ActorRef, +) => { + const handleReplaceTask = ( + { task, crumbs }: TaskAndCrumbs, + newTask: TaskDef, + ) => { + service.send({ + type: DefinitionMachineEventTypes.REPLACE_TASK_EVT, + task, + crumbs, + newTask, + }); + }; + + const handleRemoveTask = ({ task, crumbs }: TaskAndCrumbs) => { + service.send({ + type: DefinitionMachineEventTypes.REMOVE_TASK_EVT, + task, + crumbs, + }); + }; + + const handleAddSwitchPath = ({ task, crumbs }: TaskAndCrumbs) => { + service.send({ + type: DefinitionMachineEventTypes.ADD_NEW_SWITCH_PATH_EVT, + task, + crumbs, + }); + }; + + const handlePerformOperation = (operationData: { + data: OperationContext; + operation: PerformedOperation; + }) => { + service.send({ + type: DefinitionMachineEventTypes.PERFORM_OPERATION_EVT, + ...operationData, + }); + }; + + const handleRemoveBranch = ( + removeBranchRelevantData: TaskAndCrumbs & { branchName: string }, + ) => { + service.send({ + type: DefinitionMachineEventTypes.REMOVE_BRANCH_EVT, + ...removeBranchRelevantData, + }); + }; + return { + handleReplaceTask, + handleRemoveTask, + handleAddSwitchPath, + handleRemoveBranch, + handlePerformOperation, + }; +}; diff --git a/ui-next/src/pages/definition/task/CreationInfo.tsx b/ui-next/src/pages/definition/task/CreationInfo.tsx new file mode 100644 index 0000000000..cbe6463539 --- /dev/null +++ b/ui-next/src/pages/definition/task/CreationInfo.tsx @@ -0,0 +1,46 @@ +import { FunctionComponent } from "react"; +import { Stack } from "@mui/material"; +import { TaskDefinitionDto } from "types"; +import MuiTypography from "components/MuiTypography"; +import { FORMAT_TIME_TO_LONG } from "utils/constants/common"; +import { formatInTimeZone } from "utils/date"; +import _isUndefined from "lodash/isUndefined"; + +export interface CreationInfoProps { + task: Partial; +} + +export const CreationInfo: FunctionComponent = ({ + task, +}) => { + return ( + + {(!_isUndefined(task?.createTime) || !_isUndefined(task?.createdBy)) && ( + + {`Created At ${ + task.createTime + ? formatInTimeZone(new Date(task.createTime), FORMAT_TIME_TO_LONG) + : "N/A" + } by ${task.createdBy || "N/A"}`} + Created at:  + + )} + + {(!_isUndefined(task.updateTime) || !_isUndefined(task.updatedBy)) && ( + + {`Last updated at ${ + task.updateTime + ? formatInTimeZone(new Date(task.updateTime), FORMAT_TIME_TO_LONG) + : "N/A" + } by ${task.updatedBy || "N/A"}`} + + )} + + {!_isUndefined(task.ownerEmail) && ( + {`Owner email: ${ + task.ownerEmail || "N/A" + }`} + )} + + ); +}; diff --git a/ui-next/src/pages/definition/task/NameDescription.tsx b/ui-next/src/pages/definition/task/NameDescription.tsx new file mode 100644 index 0000000000..20546b9414 --- /dev/null +++ b/ui-next/src/pages/definition/task/NameDescription.tsx @@ -0,0 +1,153 @@ +import { Box, TextField } from "@mui/material"; +import { useSelector } from "@xstate/react"; +import { Text } from "components"; +import EditInPlace from "components/EditInPlace"; +import _isString from "lodash/isString"; +import { useTaskDefinitionFormActor } from "pages/definition/task/form/state/hook"; +import { TASK_FORM_MACHINE_ID } from "pages/definition/task/state/helpers"; +import { FunctionComponent, useRef } from "react"; +import { disabledInputStyle } from "shared/styles"; +import { ActorRef } from "xstate"; +import { TaskDefinitionFormMachineEvent } from "./form/state/types"; +import { + TaskDefinitionMachineEvent, + TaskDefinitionMachineState, +} from "./state/types"; + +interface NameDescriptionProps { + taskDefActor: ActorRef; +} + +const flexWrap = { + display: "flex", + alignItems: "center", + gap: 4, + flexWrap: "wrap", +}; + +interface NameDescriptionFromProps { + // This should not be like this ideally the machine should reuse the editInPLace machine like human form builder does + formActor: ActorRef; +} + +const NameDescriptionForm: FunctionComponent = ({ + formActor, +}) => { + const inputNameRef = useRef(null); + const inputDescriptionRef = useRef(null); + + const [ + { modifiedTaskDefinition, isEditingName, isEditingDescription, error }, + { handleChangeTaskForm, setEditingFieldForm }, + ] = useTaskDefinitionFormActor(formActor); + return ( + <> + setEditingFieldForm(val ? "name" : "none")} + text={modifiedTaskDefinition.name} + childRef={inputNameRef} + disabled={false} + placeholder="Type task name here" + type="input" + > + handleChangeTaskForm(event.target.value, event)} + error={!!error?.name} + helperText={error?.name?.message} + sx={{ + input: { + fontSize: "14pt", + fontWeight: "bold", + }, + ...disabledInputStyle, + }} + /> + + setEditingFieldForm(val ? "description" : "none")} + text={modifiedTaskDefinition.description} + childRef={inputDescriptionRef} + disabled={false} + placeholder="Type task description here" + type="input" + > + handleChangeTaskForm(event.target.value, event)} + value={modifiedTaskDefinition.description || ""} + error={!!error?.description} + helperText={error?.description?.message} + sx={{ + input: { + fontSize: "12pt", + }, + ...disabledInputStyle, + }} + /> + + + ); +}; + +export const NameDescription: FunctionComponent = ({ + taskDefActor, +}) => { + const isFormState = useSelector(taskDefActor, (state) => + state.matches([ + TaskDefinitionMachineState.READY, + TaskDefinitionMachineState.MAIN_CONTAINER, + TaskDefinitionMachineState.FORM, + ]), + ); + + // @ts-ignore + const formActor = taskDefActor?.children?.get(TASK_FORM_MACHINE_ID); + + const modifiedTaskDefinition = useSelector( + taskDefActor, + (state) => state.context.modifiedTaskDefinition, + ); + + return ( + + {isFormState && formActor ? ( + + ) : ( + <> + + {_isString(modifiedTaskDefinition?.name) + ? modifiedTaskDefinition?.name + : ""} + + + {_isString(modifiedTaskDefinition?.description) + ? modifiedTaskDefinition?.description + : ""} + + + )} + + ); +}; diff --git a/ui-next/src/pages/definition/task/SaveProtectionPrompt.tsx b/ui-next/src/pages/definition/task/SaveProtectionPrompt.tsx new file mode 100644 index 0000000000..318c5eab3b --- /dev/null +++ b/ui-next/src/pages/definition/task/SaveProtectionPrompt.tsx @@ -0,0 +1,204 @@ +import { useSelector } from "@xstate/react"; +import fastDeepEqual from "fast-deep-equal"; +import { FunctionComponent, useEffect, useRef } from "react"; +import BlockNavigationWithConfirmation from "shared/BlockNavigationWithConfirmation"; +import { useSaveProtection } from "shared/useSaveProtection"; +import { ActorRef } from "xstate"; +import { TaskDefinitionFormMachineEvent } from "./form/state"; +import { + TaskDefinitionMachineContext, + TaskDefinitionMachineEvent, + TaskDefinitionMachineEventType, + TaskDefinitionMachineState, +} from "./state"; +import { TASK_FORM_MACHINE_ID } from "./state/helpers"; +export interface SaveProtectionPromptProps { + taskDefActor: ActorRef; +} + +const useCheckForChanges = ( + actor: + | ActorRef + | ActorRef, +) => { + const [modifiedTaskDefinition, originTaskDefinition] = useSelector( + actor, + (state) => [ + state.context.modifiedTaskDefinition, + state.context.originTaskDefinition, + ], + ); + const result = fastDeepEqual(modifiedTaskDefinition, originTaskDefinition); + return result; +}; + +export const SaveProtectionPrompt: FunctionComponent< + SaveProtectionPromptProps +> = ({ taskDefActor }) => { + // @ts-expect-error - children type is not fully typed + const formActor = taskDefActor?.children?.get( + TASK_FORM_MACHINE_ID, + ) as ActorRef; + + const noFormChanges = useCheckForChanges( + formActor != null ? formActor : taskDefActor, + ); + + const { + showPrompt, + successfulSave, + hasErrors, + handleSave: baseHandleSave, + } = useSaveProtection< + TaskDefinitionMachineContext, + TaskDefinitionMachineEvent + >({ + actor: taskDefActor, + noFormChanges, + isSaveInProgress: (state) => { + // Check if we're in DIFF_EDITOR state (save confirmation dialog) or createTaskDefinition state (saving) + return ( + state.matches([ + TaskDefinitionMachineState.READY, + TaskDefinitionMachineState.MAIN_CONTAINER, + TaskDefinitionMachineState.DIFF_EDITOR, + ]) || + state.matches([ + TaskDefinitionMachineState.READY, + TaskDefinitionMachineState.MAIN_CONTAINER, + TaskDefinitionMachineState.DIFF_EDITOR, + "createTaskDefinition", + ]) + ); + }, + hasErrors: (state) => { + const context = state.context; + const modifiedTaskDefinition = context.modifiedTaskDefinition; + + // Check for parse errors + if (context.couldNotParseJson) { + return true; + } + + // Check for API errors + if (context.error) { + return true; + } + + // Check for required fields + if (modifiedTaskDefinition) { + // Check if name is missing or empty + if ( + !modifiedTaskDefinition.name || + modifiedTaskDefinition.name.trim() === "" + ) { + return true; + } + } + + return false; + }, + detectSaveSuccessFromEvent: (eventType) => { + // Check for cancel event + if (eventType === TaskDefinitionMachineEventType.CANCEL_CONFIRM_SAVE) { + return false; + } + return undefined; + }, + detectSaveSuccessFromContext: ({ + currentContext, + previousContext, + wasSaving, + isSaving, + }) => { + // If we were saving and now we're not, check if originTaskDefinition was updated + if (wasSaving && !isSaving && previousContext) { + const currentOriginStr = JSON.stringify( + currentContext.originTaskDefinition, + ); + const prevOriginStr = JSON.stringify( + previousContext.originTaskDefinition, + ); + + // If origin was updated, save was successful + if (currentOriginStr !== prevOriginStr) { + return true; + } + } + return false; + }, + handleSaveAction: (actor) => { + // Check current state to see if we're already in the save confirmation dialog + const snapshot = actor.getSnapshot(); + const isInSaveConfirmation = snapshot.matches([ + TaskDefinitionMachineState.READY, + TaskDefinitionMachineState.MAIN_CONTAINER, + TaskDefinitionMachineState.DIFF_EDITOR, + ]); + + // If we're already in the save confirmation dialog, trigger the save immediately + if (isInSaveConfirmation) { + actor.send({ + type: TaskDefinitionMachineEventType.SAVE_TASK_DEFINITION, + }); + } else { + // Open the save confirmation dialog + // User will need to click "Confirm Save" button in the save confirmation dialog + actor.send({ + type: TaskDefinitionMachineEventType.SET_SAVE_CONFIRMATION_OPEN, + isContinueCreate: false, + }); + } + }, + }); + + // Track last synced form data to avoid unnecessary syncing + const lastSyncedFormDataRef = useRef(null); + + // Continuously sync form data to parent context + // This ensures form data is always in sync before any re-render happens + useEffect(() => { + if (!formActor) return; + + const subscription = formActor.subscribe((state) => { + if (state.context?.modifiedTaskDefinition) { + const formDataString = JSON.stringify( + state.context.modifiedTaskDefinition, + null, + 2, + ); + + // Only sync if the data has actually changed + if (lastSyncedFormDataRef.current !== formDataString) { + lastSyncedFormDataRef.current = formDataString; + // Sync form data to parent context + taskDefActor.send({ + type: TaskDefinitionMachineEventType.HANDLE_CHANGE_TASK_DEFINITION, + modifiedTaskDefinitionString: formDataString, + }); + } + } + }); + + return () => subscription.unsubscribe(); + }, [formActor, taskDefActor]); + + const handleSave = baseHandleSave; + + return ( + + Your recent changes are not saved to the server. To run the new task, + you have to save your progress. + + } + title={"Unsaved task confirmation"} + block={showPrompt} + onSave={handleSave} + successfulSave={successfulSave} + hasErrors={hasErrors} + /> + ); +}; diff --git a/ui-next/src/pages/definition/task/TaskDefErrorInspector.tsx b/ui-next/src/pages/definition/task/TaskDefErrorInspector.tsx new file mode 100644 index 0000000000..0f09488c96 --- /dev/null +++ b/ui-next/src/pages/definition/task/TaskDefErrorInspector.tsx @@ -0,0 +1,77 @@ +import { colors } from "theme/tokens/variables"; +import AccordionSummary from "@mui/material/AccordionSummary"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import MuiTypography from "components/MuiTypography"; +import AccordionDetails from "@mui/material/AccordionDetails"; +import List from "@mui/material/List"; +import { ListItem } from "@mui/material"; +import Accordion from "@mui/material/Accordion"; + +const TaskDefErrorInspector = ({ + error, + title, +}: { + error: { [key: string]: { message: string } }; + title?: string; +}) => { + const errorKeys = error ? Object.keys(error) : []; + + return ( + theme.palette.error.main, + color: colors.white, + "&:first-of-type": { + borderTopLeftRadius: 0, + borderTopRightRadius: 0, + }, + "&.Mui-expanded": { + margin: 0, + }, + }} + > + } + aria-controls="panel1a-content" + id="panel1a-header" + sx={{ + "&.Mui-expanded": { + minHeight: 48, + }, + ".MuiAccordionSummary-content": { + margin: 0, + "&.Mui-expanded": { + margin: 0, + }, + }, + }} + > + + {title ? `${title} ` : ""}Errors ({errorKeys.length}) + + + + + {errorKeys.map((key) => ( + + + {key} + + :  + + {error[key]?.message} + + + ))} + + + + ); +}; + +export default TaskDefErrorInspector; diff --git a/ui-next/src/pages/definition/task/TaskDefinition.tsx b/ui-next/src/pages/definition/task/TaskDefinition.tsx new file mode 100644 index 0000000000..8c405d9c10 --- /dev/null +++ b/ui-next/src/pages/definition/task/TaskDefinition.tsx @@ -0,0 +1,295 @@ +import { Monaco } from "@monaco-editor/react"; +import { + Box, + CircularProgress, + LinearProgress, + Paper, + Tab, + Tabs, + Theme, +} from "@mui/material"; +import { useMachine } from "@xstate/react"; +import { DocLink } from "components/DocLink"; +import { MessageContext } from "components/v1/layout/MessageContext"; +import { ConductorSectionHeader } from "components/v1/layout/section/ConductorSectionHeader"; +import _get from "lodash/get"; +import _isString from "lodash/isString"; +import TaskDefinitionDialogs from "pages/definition/task/dialogs/TaskDefinitionDialogs"; +import TaskDefinitionFormV1 from "pages/definition/task/form/TaskDefinitionForm"; +import { taskDefinitionMachine } from "pages/definition/task/state"; +import { useTaskDefinition } from "pages/definition/task/state/hook"; +import { + TaskDefinitionMachineEventType, + TaskDefinitionMachineState, +} from "pages/definition/task/state/types"; +import { useContext, useEffect, useMemo, useRef } from "react"; +import { Helmet } from "react-helmet"; +import { useLocation, useParams } from "react-router"; +import SectionContainer from "shared/SectionContainer"; +import { useAuth } from "shared/auth"; +import { newTaskTemplate } from "templates/JSONSchemaWorkflow"; +import { colors } from "theme/tokens/variables"; +import { TaskDefinitionDto } from "types"; +import { DOC_LINK_URL } from "utils/constants/docLink"; +import { NEW_TASK_DEF_URL, TASK_DEF_URL } from "utils/constants/route"; +import { usePushHistory } from "utils/hooks/usePushHistory"; +import { useAuthHeaders } from "utils/query"; +import { randomChars } from "utils/strings"; +import { SaveProtectionPrompt } from "./SaveProtectionPrompt"; +import TaskDefinitionButtons from "./TaskDefinitionButtons"; +import TaskDefinitionDiffEditor from "./TaskDefinitionDiffEditor"; +import { TASK_DEFINITION_SAVED_SUCCESSFULLY_MESSAGE } from "./state/helpers"; + +// TODO: Should refactor this when we apply dark mode +// The dark mode styles should be configured in theme +const getBackgroundColorOfForm = ({ + theme, + isInFormView, +}: { + theme: Theme; + isInFormView: boolean; +}) => { + if (isInFormView) { + return colors.white; + } + + if (theme.palette?.mode === "dark") { + return colors.gray00; + } + + return colors.gray14; +}; + +/** + * NOTE: + * 1. Single mode: After POST successfully will redirect to task detail page + * 2. Bulk mode or Save and Create New: Stay at the same page with current state + * 3. Test task: execute a workflow with current task + * 4. Form mode doesn't have bulk creation + */ +export default function TaskDefinition() { + const pushHistory = usePushHistory(); + const { setMessage } = useContext(MessageContext); + + const { conductorUser } = useAuth(); + const authHeaders = useAuthHeaders(); + const editorRefs = useRef(null); + const location = useLocation(); + const params = useParams(); + // Memoize isNewTaskDef to prevent unnecessary re-renders when location changes but pathname stays the same + const isNewTaskDef = useMemo( + () => location.pathname === NEW_TASK_DEF_URL, + [location.pathname], + ); + + // Stabilize params to prevent unnecessary re-renders + // Only re-compute when the actual param values change, not when the params object reference changes + const paramName = _get(params, "name"); + const stableParams = useMemo(() => { + return { name: paramName }; + }, [paramName]); + + // Defines a Template and puts the name of the url. + const initTaskDefinition = useMemo( + () => ({ + ...newTaskTemplate(conductorUser?.id || "example@email.com"), + name: isNewTaskDef ? `task-${randomChars(6)}` : stableParams.name, + }), + [stableParams.name, isNewTaskDef, conductorUser?.id], + ) as Partial; + + const taskJsonString = JSON.stringify(initTaskDefinition, null, 2); + // Create Task state machine + const [current, , taskDefActor] = useMachine(taskDefinitionMachine, { + ...(process.env.NODE_ENV === "development" ? { devTools: true } : {}), + context: { + originTaskDefinitionString: taskJsonString, // Not necessary + modifiedTaskDefinitionString: taskJsonString, // Not necessary + originTaskDefinition: initTaskDefinition, + modifiedTaskDefinition: initTaskDefinition, + originTaskDefinitions: [initTaskDefinition], + isNewTaskDef, + user: conductorUser, + authHeaders, + couldNotParseJson: false, + }, + actions: { + redirectToNewTask: () => { + pushHistory(NEW_TASK_DEF_URL); + }, + redirectToEditTask: ({ modifiedTaskDefinition }) => { + pushHistory( + `${TASK_DEF_URL.BASE}/${encodeURIComponent( + modifiedTaskDefinition.name as string, + )}`, + ); + }, + redirectToTaskList: () => { + pushHistory(TASK_DEF_URL.BASE); + }, + setErrorMessage: (__, data: any) => { + setMessage({ + text: data?.data?.message, + severity: "error", + }); + }, + showSaveSuccessMessage: () => { + setMessage({ + text: TASK_DEFINITION_SAVED_SUCCESSFULLY_MESSAGE, + severity: "success", + }); + }, + }, + }); + + const [ + { + formActor, + isFetching, + modifiedTaskDefinition, + couldNotParseJson, + isReady, + }, + { toggleFormMode }, + ] = useTaskDefinition(taskDefActor); + + const isInFormView = + current.matches([ + TaskDefinitionMachineState.READY, + TaskDefinitionMachineState.MAIN_CONTAINER, + TaskDefinitionMachineState.FORM, + ]) && formActor; + + useEffect(() => { + const name = stableParams.name; + if (!isNewTaskDef && name != null) { + taskDefActor.send({ + type: TaskDefinitionMachineEventType.SET_TASK_DEFINITION, + name, + isNew: isNewTaskDef, + }); + } + }, [stableParams.name, isNewTaskDef, taskDefActor]); + + const sectionTitle = useMemo(() => { + if (isNewTaskDef) return "New Task"; + + if (_isString(modifiedTaskDefinition?.name)) { + return modifiedTaskDefinition?.name; + } + + return ""; + }, [isNewTaskDef, modifiedTaskDefinition]); + + return ( + + + + Task Definition -  + {isNewTaskDef + ? "NEW" + : _isString(modifiedTaskDefinition?.name) + ? modifiedTaskDefinition?.name + : ""} + + + + + + } + /> + } + > + {isFetching && } + + + {isReady ? ( + <> + + + toggleFormMode(!!newValue) + } + > + + + + + + + + theme.palette?.mode === "dark" + ? colors.gray14 + : undefined, + backgroundColor: (theme) => + getBackgroundColorOfForm({ + theme, + isInFormView, + }), + }} + > + {isInFormView ? ( + + ) : null} + {current.matches([ + TaskDefinitionMachineState.READY, + TaskDefinitionMachineState.MAIN_CONTAINER, + TaskDefinitionMachineState.DIFF_EDITOR, + ]) || + current.matches([ + TaskDefinitionMachineState.READY, + TaskDefinitionMachineState.MAIN_CONTAINER, + TaskDefinitionMachineState.EDITOR, + ]) ? ( + + ) : null} + + + ) : ( + + + + )} + + + + + ); +} diff --git a/ui-next/src/pages/definition/task/TaskDefinitionButtons.tsx b/ui-next/src/pages/definition/task/TaskDefinitionButtons.tsx new file mode 100644 index 0000000000..9cc9d078dc --- /dev/null +++ b/ui-next/src/pages/definition/task/TaskDefinitionButtons.tsx @@ -0,0 +1,275 @@ +import { Box, Stack, Tooltip } from "@mui/material"; +import { useSelector } from "@xstate/react"; +import Button, { MuiButtonProps } from "components/MuiButton"; +import SplitButton from "components/v1/ConductorSplitButton"; +import DownloadIcon from "components/v1/icons/DownloadIcon"; +import ResetIcon from "components/v1/icons/ResetIcon"; +import SaveIcon from "components/v1/icons/SaveIcon"; +import TrashIcon from "components/v1/icons/TrashIcon"; +import XCloseIcon from "components/v1/icons/XCloseIcon"; +import fastDeepEqual from "fast-deep-equal"; +import { TaskDefinitionFormMachineEvent } from "pages/definition/task/form/state/types"; +import { TASK_FORM_MACHINE_ID } from "pages/definition/task/state/helpers"; +import { useTaskDefinition } from "pages/definition/task/state/hook"; +import { + TaskDefinitionButtonsProps, + TaskDefinitionMachineEvent, + TaskDefinitionMachineState, +} from "pages/definition/task/state/types"; +import { FunctionComponent, useMemo } from "react"; +import { useAuth } from "shared/auth"; +import { colors } from "theme/tokens/variables"; +import { ActorRef } from "xstate"; +import { OpenTestTaskButton } from "../EditorPanel/TaskFormTab/forms/TestTaskButton/OpenTestTaskButton"; + +// Hoc to get around state for buttons +const withFormState = + ( + ButtonComponent: FunctionComponent, + actor: ActorRef, + isTrialExpired: boolean, + ) => + (buttonProps: MuiButtonProps) => { + const [modifiedTaskDefinition, originTaskDefinition, isNewTaskDef] = + useSelector(actor, (state) => [ + state.context.modifiedTaskDefinition, + state.context.originTaskDefinition, + state.context.isNewTaskDef, + ]); + const noChanges = useMemo( + () => fastDeepEqual(modifiedTaskDefinition, originTaskDefinition), + [modifiedTaskDefinition, originTaskDefinition], + ); + const isReset = buttonProps?.role === "reset"; + const resetDisabledConditions = noChanges; + const saveDisabledConditions = + (!isNewTaskDef && noChanges) || isTrialExpired; + const noDescription = !(modifiedTaskDefinition.description ?? "").trim(); + + return ( + + ); + }; + +const withEditorState = + ( + ButtonComponent: FunctionComponent, + actor: ActorRef, + isTrialExpired: boolean, + ) => + (buttonProps: MuiButtonProps) => { + const [ + modifiedTaskDefinition, + originTaskDefinition, + isNewTaskDef, + jsonInvalid, + ] = useSelector(actor, (state) => [ + state.context.modifiedTaskDefinition, + state.context.originTaskDefinition, + state.context.isNewTaskDef, + state.context.couldNotParseJson, + ]); + const noChanges = useMemo( + () => fastDeepEqual(modifiedTaskDefinition, originTaskDefinition), + [modifiedTaskDefinition, originTaskDefinition], + ); + + const isReset = buttonProps?.role === "reset"; + const resetDisabledConditions = noChanges; + const saveDisabledConditions = + jsonInvalid || (!isNewTaskDef && noChanges) || isTrialExpired; + const noDescription = !(modifiedTaskDefinition.description ?? "").trim(); + + return ( + + ); + }; + +const TaskDefinitionButtons = ({ + taskDefActor, +}: TaskDefinitionButtonsProps) => { + const [ + { + isContinueCreate, + isNewTaskDef, + saveConfirmationOpen, + couldNotParseJson, + modifiedTaskDefinition, + }, + { + cancelConfirmSave, + handleDownloadFile, + saveTaskDefinition, + setDeleteConfirmationOpen, + setResetConfirmationOpen, + setSaveConfirmationOpen, + }, + ] = useTaskDefinition(taskDefActor); + + const { isTrialExpired } = useAuth(); + + const isInForm = useSelector(taskDefActor, (state) => + state.matches([ + TaskDefinitionMachineState.READY, + TaskDefinitionMachineState.MAIN_CONTAINER, + TaskDefinitionMachineState.FORM, + ]), + ); + + // @ts-ignore + const formActor = taskDefActor?.children?.get(TASK_FORM_MACHINE_ID); + + const SaveResetButton = + isInForm && formActor + ? withFormState(Button, formActor, isTrialExpired) + : withEditorState(Button, taskDefActor, isTrialExpired); + + const saveSplitButtonOptions = [ + { + id: "task-save-and-create-new-btn", + label: "Save & Create New", + onClick: () => setSaveConfirmationOpen(true), + }, + ]; + + const suffix = Math.random().toString(36).substring(2, 5); + const taskDefinition = { + name: modifiedTaskDefinition?.name, + taskReferenceName: `test_task_${modifiedTaskDefinition?.name}_${suffix}`, + type: "SIMPLE", + inputParameters: {}, + }; + + return ( + + theme.palette?.mode === "dark" ? colors.gray14 : undefined, + backgroundColor: (theme) => + theme.palette?.mode === "dark" ? colors.gray00 : colors.gray14, + }} + > + + theme.palette?.mode === "dark" ? colors.gray03 : colors.gray12, + }} + > + {saveConfirmationOpen ? ( + + + + + ) : ( + + {!isNewTaskDef && ( + + + + )} + + } + > + Reset + + + + + + + + {isNewTaskDef ? ( + } + id="task-save-btn" + options={saveSplitButtonOptions} + primaryOnClick={() => setSaveConfirmationOpen(false)} + tooltip="Save this definition" + data-testid="task-definition-save-button" + disabled={isTrialExpired} + > + Save + + ) : ( + setSaveConfirmationOpen(false)} + startIcon={} + > + Save + + )} + + )} + + + ); +}; +export default TaskDefinitionButtons; diff --git a/ui-next/src/pages/definition/task/TaskDefinitionDiffEditor.tsx b/ui-next/src/pages/definition/task/TaskDefinitionDiffEditor.tsx new file mode 100644 index 0000000000..943a7537da --- /dev/null +++ b/ui-next/src/pages/definition/task/TaskDefinitionDiffEditor.tsx @@ -0,0 +1,141 @@ +import Editor, { Monaco } from "@monaco-editor/react"; +import { Box } from "@mui/material"; +import { DiffEditor } from "components/DiffEditor/DiffEditor"; +import { useTaskDefinition } from "pages/definition/task/state/hook"; +import { TaskDefinitionDiffEditorProps } from "pages/definition/task/state/types"; +import { + ForwardedRef, + forwardRef, + useCallback, + useContext, + useImperativeHandle, + useRef, +} from "react"; +import { defaultEditorOptions } from "shared/editor"; +import { ColorModeContext } from "theme/material/ColorModeContext"; +import { + configureMonaco, + JSON_FILE_TASK_NAME, +} from "utils/monacoUtils/CodeEditorUtils"; + +const minEditor_Width = 590; +const TaskDefinitionDiffEditor = ( + { taskDefActor }: TaskDefinitionDiffEditorProps, + editorRefs: ForwardedRef, +) => { + const { mode } = useContext(ColorModeContext); + const monacoObjects = useRef(null); + const diffMonacoObjects = useRef(null); + const [ + { + isNewTaskDef, + modifiedTaskDefinitionString, + originTaskDefinitionString, + isConfirmingSave, + }, + { handleChangeTaskDefinition }, + ] = useTaskDefinition(taskDefActor); + + // const errorKeys = error ? Object.keys(error) : []; + useImperativeHandle( + editorRefs, + () => ({ + reset: (value: string) => { + monacoObjects.current.setValue(value); + diffMonacoObjects.current.getModel().modified.setValue(value); + }, + getValue: () => { + return monacoObjects.current.getValue(); + }, + code: monacoObjects.current, + diff: diffMonacoObjects.current, + }), + [monacoObjects, diffMonacoObjects], + ); + const darkMode = mode === "dark"; + const editorTheme = darkMode ? "vs-dark" : "vs-light"; + const editorState = { + editorOptions: { + ...defaultEditorOptions, + selectOnLineNumbers: true, + }, + } as Monaco; + const editorDidMount = useCallback( + (editor: Monaco) => { + monacoObjects.current = editor; + }, + [monacoObjects], + ); + const diffEditorDidMount = useCallback( + (editor: Monaco) => { + diffMonacoObjects.current = editor; + const modifiedEditor = editor.getModifiedEditor(); + modifiedEditor.onDidChangeModelContent((_: any) => { + const maybeText = modifiedEditor.getValue(); + if (typeof maybeText === "string") { + handleChangeTaskDefinition(maybeText); + } + }); + }, + [diffMonacoObjects, handleChangeTaskDefinition], + ); + const handleEditorWillMount = useCallback((monaco: Monaco) => { + configureMonaco(monaco); + }, []); + + return ( + <> + + + {isConfirmingSave ? ( + + ) : ( + { + if (typeof maybeText === "string") { + handleChangeTaskDefinition(maybeText); + } + }} + path={JSON_FILE_TASK_NAME} + /> + )} + + + + ); +}; +export default forwardRef(TaskDefinitionDiffEditor); diff --git a/ui-next/src/pages/definition/task/TestTaskForm.tsx b/ui-next/src/pages/definition/task/TestTaskForm.tsx new file mode 100644 index 0000000000..8ff6066f12 --- /dev/null +++ b/ui-next/src/pages/definition/task/TestTaskForm.tsx @@ -0,0 +1,105 @@ +import { Box, Grid, Link } from "@mui/material"; +import { Button } from "components/index"; +import { Play } from "@phosphor-icons/react"; +import MuiTypography from "components/MuiTypography"; +import ConductorInput from "components/v1/ConductorInput"; +import { WORKFLOW_EXECUTION_URL } from "utils/constants/route"; +import { ConductorCodeBlockInput } from "components/v1/ConductorCodeBlockInput"; + +export type TestTaskFormProps = { + handleRunTestTask: () => void; + isNewTaskDef: boolean; + setInputParameters: (value: string) => void; + setTaskDomain: (value: string) => void; + showTestTask: boolean; + testInputParameters: string; + testTaskDomain: string; + testTaskWorkflowId: string; +}; + +const TestTaskForm = ({ + handleRunTestTask, + isNewTaskDef, + setInputParameters, + setTaskDomain, + showTestTask, + testInputParameters, + testTaskDomain, + testTaskWorkflowId, +}: TestTaskFormProps) => { + return !isNewTaskDef && showTestTask ? ( + + { + <> + + { + setInputParameters(value); + }} + /> + + + setTaskDomain(event.target.value)} + placeholder="Enter domain" + /> + + + + + {testTaskWorkflowId ? ( + + Workflow started at: + + + {testTaskWorkflowId} + + + + ) : null} + + + + } + + ) : null; +}; + +export default TestTaskForm; diff --git a/ui-next/src/pages/definition/task/dialogs/TaskDefinitionDialogs.tsx b/ui-next/src/pages/definition/task/dialogs/TaskDefinitionDialogs.tsx new file mode 100644 index 0000000000..45faba30cb --- /dev/null +++ b/ui-next/src/pages/definition/task/dialogs/TaskDefinitionDialogs.tsx @@ -0,0 +1,79 @@ +import { useSelector } from "@xstate/react"; +import ConfirmChoiceDialog from "components/ConfirmChoiceDialog"; +import { TaskDefinitionDialogsProps } from "pages/definition/task/dialogs/state"; +import { + TaskDefinitionMachineState, + TaskDefinitionMachineEventType, +} from "pages/definition/task/state/types"; +// +const TaskDefinitionDialogs = ({ + taskDefActor, +}: TaskDefinitionDialogsProps) => { + const isConfirmReset = useSelector(taskDefActor, (state) => + state.matches([ + TaskDefinitionMachineState.READY, + TaskDefinitionMachineState.RESET_FORM, + TaskDefinitionMachineState.RESET_FORM_CONFIRM, + ]), + ); + + const isConfirmDelete = useSelector(taskDefActor, (state) => + state.matches([ + TaskDefinitionMachineState.READY, + TaskDefinitionMachineState.DELETE_FORM, + TaskDefinitionMachineState.DELETE_FORM_CONFIRM, + ]), + ); + + const originTaskDefinition = useSelector( + taskDefActor, + (state) => state.context.originTaskDefinition, + ); + + const handleResetConfirmation = (val: boolean) => { + taskDefActor.send({ + type: val + ? TaskDefinitionMachineEventType.CONFIRM_RESET_TASK + : TaskDefinitionMachineEventType.CANCEL_CONFIRM_SAVE, + }); + }; + + return ( + <> + {isConfirmReset ? ( + + ) : null} + + {isConfirmDelete && ( + + Are you sure you want to delete  + + {originTaskDefinition?.name} + +  task definition? This change cannot be undone. +
    + Please type  + {originTaskDefinition?.name} +  to confirm. +
    + + } + header={"Deletion Confirmation"} + isInputConfirmation + valueToBeDeleted={originTaskDefinition?.name} + /> + )} + + ); +}; + +export default TaskDefinitionDialogs; diff --git a/ui-next/src/pages/definition/task/dialogs/state/actions.ts b/ui-next/src/pages/definition/task/dialogs/state/actions.ts new file mode 100644 index 0000000000..c86ab35f9d --- /dev/null +++ b/ui-next/src/pages/definition/task/dialogs/state/actions.ts @@ -0,0 +1,14 @@ +import { sendParent } from "xstate"; +import { TaskDefinitionDialogsMachineType } from "pages/definition/task/dialogs/state/types"; + +export const notifyResetTask = sendParent({ + type: TaskDefinitionDialogsMachineType.CONFIRM_RESET_TASK, +}); + +export const notifyDeleteTask = sendParent({ + type: TaskDefinitionDialogsMachineType.CONFIRM_DELETE_TASK, +}); + +export const notifyGoToDefineNewTask = sendParent({ + type: TaskDefinitionDialogsMachineType.CONFIRM_GO_TO_DEFINE_NEW_TASK, +}); diff --git a/ui-next/src/pages/definition/task/dialogs/state/guards.ts b/ui-next/src/pages/definition/task/dialogs/state/guards.ts new file mode 100644 index 0000000000..199f89b86a --- /dev/null +++ b/ui-next/src/pages/definition/task/dialogs/state/guards.ts @@ -0,0 +1,16 @@ +import { + HandleDefineNewConfirmationEvent, + HandleDeleteTaskDefConfirmationEvent, + HandleResetConfirmationEvent, + TaskDefinitionDialogsContext, +} from "pages/definition/task/dialogs/state/types"; + +export const isConfirm = ( + _: TaskDefinitionDialogsContext, + event: + | HandleDefineNewConfirmationEvent + | HandleDeleteTaskDefConfirmationEvent + | HandleResetConfirmationEvent, +) => { + return event.isConfirm; +}; diff --git a/ui-next/src/pages/definition/task/dialogs/state/hook.ts b/ui-next/src/pages/definition/task/dialogs/state/hook.ts new file mode 100644 index 0000000000..7d1e18d753 --- /dev/null +++ b/ui-next/src/pages/definition/task/dialogs/state/hook.ts @@ -0,0 +1,61 @@ +import { ActorRef } from "xstate"; +import { + TaskDefinitionDialogsMachineEvent, + TaskDefinitionDialogsMachineType, +} from "pages/definition/task/dialogs/state/types"; +import { useActor } from "@xstate/react"; + +export const useTaskDefinitionDialogs = ( + actor: ActorRef, +) => { + const [state, send] = useActor(actor); + + const confirmationDialogResetOpen = state.matches( + "confirmationDialogResetOpen", + ); + + const confirmationDialogDefineNewOpen = state.matches( + "confirmationDialogDefineNewOpen", + ); + + const confirmationDialogDeleteOpen = state.matches( + "confirmationDialogDeleteOpen", + ); + + const modifiedTaskDefinition = state.context.modifiedTaskDefinition; + + const handleResetConfirmation = (isConfirm: boolean) => { + send({ + type: TaskDefinitionDialogsMachineType.HANDLE_RESET_CONFIRMATION, + isConfirm, + }); + }; + + const handleDefineNewConfirmation = (isConfirm: boolean) => { + send({ + type: TaskDefinitionDialogsMachineType.HANDLE_DEFINE_NEW_CONFIRMATION, + isConfirm, + }); + }; + + const handleDeleteTaskDefConfirmation = (isConfirm: boolean) => { + send({ + type: TaskDefinitionDialogsMachineType.HANDLE_DELETE_TASK_DEF_CONFIRMATION, + isConfirm, + }); + }; + + return [ + { + confirmationDialogDefineNewOpen, + confirmationDialogDeleteOpen, + confirmationDialogResetOpen, + modifiedTaskDefinition, + }, + { + handleDefineNewConfirmation, + handleDeleteTaskDefConfirmation, + handleResetConfirmation, + }, + ] as const; +}; diff --git a/ui-next/src/pages/definition/task/dialogs/state/index.ts b/ui-next/src/pages/definition/task/dialogs/state/index.ts new file mode 100644 index 0000000000..8362004ed5 --- /dev/null +++ b/ui-next/src/pages/definition/task/dialogs/state/index.ts @@ -0,0 +1,3 @@ +export * from "./actions"; +export * from "./machine"; +export * from "./types"; diff --git a/ui-next/src/pages/definition/task/dialogs/state/machine.ts b/ui-next/src/pages/definition/task/dialogs/state/machine.ts new file mode 100644 index 0000000000..c720200e61 --- /dev/null +++ b/ui-next/src/pages/definition/task/dialogs/state/machine.ts @@ -0,0 +1,96 @@ +import { createMachine } from "xstate"; +import * as customActions from "./actions"; +import * as guards from "./guards"; +import { TaskDefinitionDto } from "types"; +import { TASK_DIALOGS_MACHINE_ID } from "pages/definition/task/state/helpers"; +import { + TaskDefinitionDialogsContext, + TaskDefinitionDialogsMachineEvent, + TaskDefinitionDialogsMachineType, +} from "pages/definition/task/dialogs/state/types"; + +export const taskDefinitionDialogsMachine = createMachine< + TaskDefinitionDialogsContext, + TaskDefinitionDialogsMachineEvent +>( + { + id: TASK_DIALOGS_MACHINE_ID, + predictableActionArguments: true, + initial: "idle", + context: { + modifiedTaskDefinition: {} as TaskDefinitionDto, + originTaskDefinition: {} as TaskDefinitionDto, + }, + on: { + [TaskDefinitionDialogsMachineType.SET_DEFINE_NEW_TASK_OPEN]: [ + { + target: "confirmationDialogDefineNewOpen", + }, + ], + [TaskDefinitionDialogsMachineType.SET_RESET_CONFIRMATION_OPEN]: [ + { + target: "confirmationDialogResetOpen", + }, + ], + [TaskDefinitionDialogsMachineType.SET_DELETE_CONFIRMATION_OPEN]: [ + { + target: "confirmationDialogDeleteOpen", + }, + ], + }, + states: { + idle: {}, + confirmationDialogDefineNewOpen: { + on: { + [TaskDefinitionDialogsMachineType.HANDLE_DEFINE_NEW_CONFIRMATION]: [ + { + cond: "isConfirm", + actions: ["notifyGoToDefineNewTask"], + target: `#${TASK_DIALOGS_MACHINE_ID}.finish`, + }, + { + target: `#${TASK_DIALOGS_MACHINE_ID}.finish`, + }, + ], + }, + }, + confirmationDialogResetOpen: { + on: { + [TaskDefinitionDialogsMachineType.HANDLE_RESET_CONFIRMATION]: [ + { + cond: "isConfirm", + actions: ["notifyResetTask"], + target: `#${TASK_DIALOGS_MACHINE_ID}.finish`, + }, + { + target: `#${TASK_DIALOGS_MACHINE_ID}.finish`, + }, + ], + }, + }, + confirmationDialogDeleteOpen: { + on: { + [TaskDefinitionDialogsMachineType.HANDLE_DELETE_TASK_DEF_CONFIRMATION]: + [ + { + cond: "isConfirm", + actions: ["notifyDeleteTask"], + target: `#${TASK_DIALOGS_MACHINE_ID}.finish`, + }, + { + target: `#${TASK_DIALOGS_MACHINE_ID}.finish`, + }, + ], + }, + }, + finish: { + type: "final", + data: (_, event) => ({ event }), + }, + }, + }, + { + actions: customActions as any, + guards: guards as any, + }, +); diff --git a/ui-next/src/pages/definition/task/dialogs/state/types.ts b/ui-next/src/pages/definition/task/dialogs/state/types.ts new file mode 100644 index 0000000000..ad77489992 --- /dev/null +++ b/ui-next/src/pages/definition/task/dialogs/state/types.ts @@ -0,0 +1,81 @@ +import { ActorRef } from "xstate"; +import { TaskDefinitionMachineEvent } from "pages/definition/task/state"; +import { TaskDefinitionDto } from "types/TaskDefinition"; + +export enum TaskDefinitionDialogsMachineType { + HANDLE_DEFINE_NEW_CONFIRMATION = "HANDLE_DEFINE_NEW_CONFIRMATION", + HANDLE_DELETE_TASK_DEF_CONFIRMATION = "HANDLE_DELETE_TASK_DEF_CONFIRMATION", + HANDLE_RESET_CONFIRMATION = "HANDLE_RESET_CONFIRMATION", + SET_DEFINE_NEW_TASK_OPEN = "SET_DEFINE_NEW_TASK_OPEN", + SET_DELETE_CONFIRMATION_OPEN = "SET_DELETE_CONFIRMATION_OPEN", + SET_RESET_CONFIRMATION_OPEN = "SET_RESET_CONFIRMATION_OPEN", + CONFIRM_DELETE_TASK = "CONFIRM_DELETE_TASK", + CONFIRM_GO_TO_DEFINE_NEW_TASK = "CONFIRM_GO_TO_DEFINE_NEW_TASK", + CONFIRM_RESET_TASK = "CONFIRM_RESET_TASK", +} + +export interface TaskDefinitionDialogsContext { + modifiedTaskDefinition: TaskDefinitionDto; + originTaskDefinition: TaskDefinitionDto; +} + +export type SetDefineNewTaskOpenEvent = { + type: TaskDefinitionDialogsMachineType.SET_DEFINE_NEW_TASK_OPEN; +}; + +export type SetDeleteConfirmationOpenEvent = { + type: TaskDefinitionDialogsMachineType.SET_DELETE_CONFIRMATION_OPEN; +}; + +export type SetResetConfirmationOpenEvent = { + type: TaskDefinitionDialogsMachineType.SET_RESET_CONFIRMATION_OPEN; +}; + +export type HandleResetConfirmationEvent = { + type: TaskDefinitionDialogsMachineType.HANDLE_RESET_CONFIRMATION; + isConfirm: boolean; +}; + +export type HandleDefineNewConfirmationEvent = { + type: TaskDefinitionDialogsMachineType.HANDLE_DEFINE_NEW_CONFIRMATION; + isConfirm: boolean; +}; + +export type HandleDeleteTaskDefConfirmationEvent = { + type: TaskDefinitionDialogsMachineType.HANDLE_DELETE_TASK_DEF_CONFIRMATION; + isConfirm: boolean; +}; + +export type ConfirmDeleteTaskEvent = { + type: TaskDefinitionDialogsMachineType.CONFIRM_DELETE_TASK; +}; + +export type ConfirmGoToDefineNewTask = { + type: TaskDefinitionDialogsMachineType.CONFIRM_GO_TO_DEFINE_NEW_TASK; +}; + +export type ConfirmResetTaskEvent = { + type: TaskDefinitionDialogsMachineType.CONFIRM_RESET_TASK; +}; + +export type TaskDefinitionDialogsMachineEvent = + | ConfirmDeleteTaskEvent + | ConfirmGoToDefineNewTask + | ConfirmResetTaskEvent + | HandleDefineNewConfirmationEvent + | HandleDeleteTaskDefConfirmationEvent + | HandleResetConfirmationEvent + | SetDefineNewTaskOpenEvent + | SetDeleteConfirmationOpenEvent + | SetResetConfirmationOpenEvent; + +export interface TaskDefinitionDialogsProps { + taskDefActor: ActorRef; +} + +export interface TaskDefinitionDialogsFinalContext { + event: + | HandleDefineNewConfirmationEvent + | HandleDeleteTaskDefConfirmationEvent + | HandleResetConfirmationEvent; +} diff --git a/ui-next/src/pages/definition/task/form/TaskDefinitionForm.tsx b/ui-next/src/pages/definition/task/form/TaskDefinitionForm.tsx new file mode 100644 index 0000000000..11d0fadc81 --- /dev/null +++ b/ui-next/src/pages/definition/task/form/TaskDefinitionForm.tsx @@ -0,0 +1,583 @@ +import { + Box, + FormControlLabel, + Grid, + GridProps, + Link, + Switch, +} from "@mui/material"; +import MuiTypography from "components/MuiTypography"; +import { ConductorArrayFieldBase } from "components/v1/ConductorArrayField"; +import ConductorInput from "components/v1/ConductorInput"; +import ConductorInputNumber from "components/v1/ConductorInputNumber"; +import ConductorSelect from "components/v1/ConductorSelect"; +import { ConductorFlatMapFormBase } from "components/v1/FlatMapForm/ConductorFlatMapForm"; +import _ from "lodash"; +import _isArray from "lodash/isArray"; +import { useTaskDefinitionFormActor } from "pages/definition/task/form/state/hook"; +import { + TaskDefinitionFormProps, + TaskRetryLogic, + TaskRetryLogicLabel, + TaskTimeoutPolicy, + TaskTimeoutPolicyLabel, +} from "pages/definition/task/state"; +import { ConductorNameVersionField } from "components/v1/ConductorNameVersionField"; +import { SchemaDefinition } from "types/SchemaDefinition"; +import { handleValidChars } from "utils"; +import { TASK_NAME_REGEX, regexToString } from "utils/constants/regex"; + +const gridContainerItemProps: GridProps = { + container: true, + size: { + xs: 12, + sm: 12, + md: 6, + }, + spacing: 3, + width: "100%", +}; + +const forceToArrayIfWrongType = (val: any) => (_isArray(val) ? val : []); + +const TaskDefinitionForm = ({ formActor }: TaskDefinitionFormProps) => { + const [ + { modifiedTaskDefinition, error }, + { handleChangeTaskForm, handleChangeParameters, handleChangeInputForm }, + ] = useTaskDefinitionFormActor(formActor); + + const isLinearBackoff = + modifiedTaskDefinition.retryLogic === TaskRetryLogic.LINEAR_BACKOFF; + const isBackoff = + modifiedTaskDefinition.retryLogic === TaskRetryLogic.EXPONENTIAL_BACKOFF || + isLinearBackoff; + + return ( + + + + + + + Basic settings + + + + handleChangeInputForm("name", value), + regexToString(TASK_NAME_REGEX), + )} + value={modifiedTaskDefinition.name} + error={!!error?.name} + helperText={error?.name?.message} + id="task-name-field" + placeholder="Enter task name" + /> + + + + handleChangeInputForm("description", value) + } + value={modifiedTaskDefinition.description} + error={ + !!error?.description || !modifiedTaskDefinition.description + } + helperText={error?.description?.message} + required + autoFocus + placeholder="Enter description" + sx={{ + "& .MuiInputBase-root": { + alignItems: "flex-start", + }, + }} + /> + + + + + + + + + + + Rate limit settings + + + + + + + + + + + + + + + + + + + Retry settings + + + + + + + + + + + handleChangeTaskForm(event.target.value, event) + } + value={modifiedTaskDefinition.retryLogic} + tooltip={{ + title: "Retry policy", + content: "The mechanism for retries.", + }} + items={Object.values(TaskRetryLogic).map((value) => ({ + label: TaskRetryLogicLabel[value], + value, + }))} + /> + + + + + + + + + Timeout settings + + + + + + + + + + + + + + handleChangeTaskForm(event.target.value, event) + } + value={modifiedTaskDefinition.timeoutPolicy} + fullWidth + tooltip={{ + title: "Timeout policy", + content: "The policy for handling timeout", + }} + items={Object.values(TaskTimeoutPolicy).map((value) => ({ + label: TaskTimeoutPolicyLabel[value], + value, + }))} + /> + + + + + + Schema + + + JSON schema for the input/output validation.{" "} + + Learn more. + + + + + + + + _.chain(data) + .groupBy("name") + .map((group, key) => ({ + name: key, + versions: _.map(group, "version"), + })) + .value() + } + value={modifiedTaskDefinition.inputSchema} + onChange={(value) => { + if (value) { + handleChangeInputForm("inputSchema", { + ...value, + type: "JSON", + }); + } else { + handleChangeInputForm("inputSchema", undefined); + } + }} + /> + + + + + _.chain(data) + .groupBy("name") + .map((group, key) => ({ + name: key, + versions: _.map(group, "version"), + })) + .value() + } + value={modifiedTaskDefinition.outputSchema} + onChange={(value) => { + if (value) { + handleChangeInputForm("outputSchema", { + ...value, + type: "JSON", + }); + } else { + handleChangeInputForm("outputSchema", undefined); + } + }} + /> + + + + + handleChangeInputForm("enforceSchema", checked) + } + /> + } + label="Enforce schema" + /> + + + + + + + Task input template: + + + These values act as the task's default input when added to the + workflow and can be overridden within a workflow.{" "} + + Learn more. + + + + + + + { + handleChangeParameters({ + name: "inputTemplate", + value: newValues, + }); + }} + value={{ ...modifiedTaskDefinition.inputTemplate }} + title="" + typeColumnLabel="Type" + keyColumnLabel="Key" + valueColumnLabel="Value" + addItemLabel="Add parameter" + showFieldTypes + enableAutocomplete={false} + /> + + + + + + + Input keys: + + + These values serve as an indicator of the expected input for the + task. + + + + + { + handleChangeParameters({ + name: "inputKeys", + value: newValues, + }); + }} + value={forceToArrayIfWrongType(modifiedTaskDefinition.inputKeys)} + inputLabel="Key" + placeholder="e.g: some key..." + addButtonLabel="Add key" + /> + + + + + + Output keys: + + + These values serve as an indicator of the expected output from the + task. + + + + + { + handleChangeParameters({ + name: "outputKeys", + value: newValues, + }); + }} + value={forceToArrayIfWrongType(modifiedTaskDefinition.outputKeys)} + inputLabel="Key" + placeholder="e.g: some key..." + addButtonLabel="Add key" + /> + + + + ); +}; + +export default TaskDefinitionForm; diff --git a/ui-next/src/pages/definition/task/form/state/actions.ts b/ui-next/src/pages/definition/task/form/state/actions.ts new file mode 100644 index 0000000000..241e3fe5fb --- /dev/null +++ b/ui-next/src/pages/definition/task/form/state/actions.ts @@ -0,0 +1,49 @@ +import { assign, DoneInvokeEvent } from "xstate"; +import { + HandleChangeTaskFormEvent, + TaskDefinitionFormContext, +} from "pages/definition/task/form/state/types"; + +export const handleChangeTask = assign< + TaskDefinitionFormContext, + HandleChangeTaskFormEvent +>(({ modifiedTaskDefinition }, { name, value }) => { + // FIXME: Remove this patch after applying new inputs + // Don't need to check array or object anymore + const isArray = ["inputKeys", "outputKeys"].some((key) => key === name); + const result = { + ...modifiedTaskDefinition, + [name]: + isArray && typeof value === "object" && !Array.isArray(value) + ? Object.keys(value!) + : value, + }; + + return { + modifiedTaskDefinition: result, + modifiedTaskDefinitionString: JSON.stringify(result, null, 2), + }; +}); + +export const persistError = assign< + TaskDefinitionFormContext, + DoneInvokeEvent<{ error: { [key: string]: any }; numberOfError: number }> +>((context, { data }) => ({ + error: data.error, + numberOfError: data.numberOfError, +})); + +export const persistErrorMessage = assign< + TaskDefinitionFormContext, + DoneInvokeEvent<{ message: string }> +>((context, { data }) => ({ + popoverMessage: { severity: "error", text: data.message }, +})); + +export const resetForm = assign( + ({ originTaskDefinition }) => ({ + modifiedTaskDefinition: originTaskDefinition, + error: undefined, + numberOfError: undefined, + }), +); diff --git a/ui-next/src/pages/definition/task/form/state/guards.ts b/ui-next/src/pages/definition/task/form/state/guards.ts new file mode 100644 index 0000000000..0ae731a182 --- /dev/null +++ b/ui-next/src/pages/definition/task/form/state/guards.ts @@ -0,0 +1,11 @@ +import { SetEditingFormFieldEvent, TaskDefinitionFormContext } from "./types"; + +export const isNameField = ( + context: TaskDefinitionFormContext, + { name }: SetEditingFormFieldEvent, +) => name === "name"; + +export const isDescriptionField = ( + context: TaskDefinitionFormContext, + { name }: SetEditingFormFieldEvent, +) => name === "description"; diff --git a/ui-next/src/pages/definition/task/form/state/hook.ts b/ui-next/src/pages/definition/task/form/state/hook.ts new file mode 100644 index 0000000000..bc7965d28b --- /dev/null +++ b/ui-next/src/pages/definition/task/form/state/hook.ts @@ -0,0 +1,91 @@ +import { ActorRef } from "xstate"; +import { + TaskDefinitionFormEventType, + TaskDefinitionFormMachineEvent, +} from "./types"; +import { useActor, useSelector } from "@xstate/react"; +import { ChangeEvent } from "react"; + +export const useTaskDefinitionFormActor = ( + actor: ActorRef, +) => { + const [state, send] = useActor(actor); + const { modifiedTaskDefinition, originTaskDefinition, error } = state.context; + + const isEditingName = useSelector(actor, (state) => + state.matches("ready.editingField.name"), + ); + + const isEditingDescription = useSelector(actor, (state) => + state.matches("ready.editingField.description"), + ); + + const handleChangeTaskForm = ( + value: number | string | Record | null, + event?: ChangeEvent, + ) => { + if (event) { + const { name } = event.target; + + send({ + type: TaskDefinitionFormEventType.HANDLE_CHANGE_TASK_FORM, + name, + value, + }); + } + }; + + const handleChangeInputForm = ( + name: string, + value: + | number + | string + | Record + | boolean + | null + | undefined, + ) => { + send({ + type: TaskDefinitionFormEventType.HANDLE_CHANGE_TASK_FORM, + name, + value, + }); + }; + + const handleChangeParameters = ({ + name, + value, + }: { + name: string; + value: Record | string[]; + }) => { + send({ + type: TaskDefinitionFormEventType.HANDLE_CHANGE_TASK_FORM, + name, + value, + }); + }; + + const setEditingFieldForm = (name: string) => { + send({ + type: TaskDefinitionFormEventType.SET_EDITING_FORM_FIELD, + name, + }); + }; + + return [ + { + error, + isEditingName, + isEditingDescription, + modifiedTaskDefinition, + originTaskDefinition, + }, + { + handleChangeTaskForm, + handleChangeParameters, + setEditingFieldForm, + handleChangeInputForm, + }, + ] as const; +}; diff --git a/ui-next/src/pages/definition/task/form/state/index.ts b/ui-next/src/pages/definition/task/form/state/index.ts new file mode 100644 index 0000000000..3a185609b7 --- /dev/null +++ b/ui-next/src/pages/definition/task/form/state/index.ts @@ -0,0 +1,4 @@ +export * from "./machine"; +export * from "./types"; +export * from "./guards"; +export * from "./actions"; diff --git a/ui-next/src/pages/definition/task/form/state/machine.ts b/ui-next/src/pages/definition/task/form/state/machine.ts new file mode 100644 index 0000000000..a835c50411 --- /dev/null +++ b/ui-next/src/pages/definition/task/form/state/machine.ts @@ -0,0 +1,133 @@ +import { createMachine } from "xstate"; +import { + TaskDefinitionFormContext, + TaskDefinitionFormEventType, + TaskDefinitionFormMachineEvent, +} from "pages/definition/task/form/state/types"; +import * as customActions from "./actions"; +import * as guards from "./guards"; +import * as services from "./services"; +import { TaskDefinitionDto } from "types"; +import { TASK_FORM_MACHINE_ID } from "pages/definition/task/state/helpers"; + +export const taskDefinitionFormMachine = createMachine< + TaskDefinitionFormContext, + TaskDefinitionFormMachineEvent +>( + { + id: TASK_FORM_MACHINE_ID, + predictableActionArguments: true, + initial: "ready", + context: { + modifiedTaskDefinition: {} as TaskDefinitionDto, + originTaskDefinition: {} as TaskDefinitionDto, + }, + on: { + [TaskDefinitionFormEventType.SET_EDITING_FORM_FIELD]: [ + // This is not needed and complex but works. Will refactor later + { + target: "ready.editingField.name", + cond: "isNameField", + }, + { + target: "ready.editingField.description", + cond: "isDescriptionField", + }, + { + target: "ready.editingField.none", + }, + ], + }, + states: { + ready: { + type: "parallel", + on: { + [TaskDefinitionFormEventType.HANDLE_CHANGE_TASK_FORM]: { + actions: "handleChangeTask", + /* target: ".validate.start", */ + }, + [TaskDefinitionFormEventType.TOGGLE_FORM_MODE]: { + target: "finish", + }, + [TaskDefinitionFormEventType.SET_SAVE_CONFIRMATION_OPEN]: { + target: "finish", + }, + [TaskDefinitionFormEventType.SET_RESET_CONFIRMATION_OPEN]: { + target: "finish", + }, + [TaskDefinitionFormEventType.SET_DELETE_CONFIRMATION_OPEN]: { + target: "finish", + }, + [TaskDefinitionFormEventType.CONFIRM_RESET_TASK]: { + actions: ["resetForm"], + }, + [TaskDefinitionFormEventType.RESET_FORM]: { + actions: "resetForm", + }, + }, + states: { + exportFileState: { + //No need to be a parallel state.[nor the others] but big refactor will handle this at a later time + initial: "idle", + states: { + idle: { + on: { + [TaskDefinitionFormEventType.EXPORT_TASK_TO_JSON_FILE]: { + target: "exportTask", + }, + }, + }, + exportTask: { + invoke: { + src: "handleDownloadFile", + onDone: { + target: "idle", + }, + onError: { + actions: ["persistErrorMessage"], + }, + }, + }, + }, + }, + editingField: { + initial: "none", + states: { + none: {}, + name: {}, + description: {}, + }, + }, + validate: { + initial: "stop", + states: { + stop: {}, + start: { + invoke: { + src: "validateForm", + onDone: { + actions: "persistError", + target: "stop", + }, + onError: { + actions: "persistErrorMessage", + target: "stop", + }, + }, + }, + }, + }, + }, + }, + finish: { + type: "final", + data: (context, event) => ({ ...context, reason: event.type }), + }, + }, + }, + { + actions: customActions as any, + guards: guards as any, + services: services as any, + }, +); diff --git a/ui-next/src/pages/definition/task/form/state/services.ts b/ui-next/src/pages/definition/task/form/state/services.ts new file mode 100644 index 0000000000..bb8a9673d3 --- /dev/null +++ b/ui-next/src/pages/definition/task/form/state/services.ts @@ -0,0 +1,12 @@ +import { TaskDefinitionFormContext } from "pages/definition/task/form/state/types"; + +import { handleDownloadFile } from "pages/definition/task/state/services"; +import { validatingService } from "pages/definition/task/state/validator"; + +export const validateForm = async ({ + modifiedTaskDefinition, +}: TaskDefinitionFormContext) => { + return validatingService(modifiedTaskDefinition, false); +}; + +export { handleDownloadFile }; diff --git a/ui-next/src/pages/definition/task/form/state/types.ts b/ui-next/src/pages/definition/task/form/state/types.ts new file mode 100644 index 0000000000..c33aed2eb6 --- /dev/null +++ b/ui-next/src/pages/definition/task/form/state/types.ts @@ -0,0 +1,90 @@ +import { PopoverMessage, TaskDefinitionDto } from "types"; + +export interface TaskDefinitionFormContext { + modifiedTaskDefinition: TaskDefinitionDto; + originTaskDefinition: TaskDefinitionDto; + error?: { [key: string]: any }; + numberOfError?: number; + popoverMessage?: PopoverMessage; +} + +export enum TaskDefinitionFormEventType { + HANDLE_CHANGE_TASK_FORM = "HANDLE_CHANGE_TASK_FORM", + SET_EDITING_FORM_FIELD = "SET_EDITING_FORM_FIELD", + STOP_FORM_MACHINE = "STOP_FORM_MACHINE", + SYNC_DATA_TO_PARENT = "SYNC_DATA_TO_PARENT", + RESET_FORM = "RESET_FORM", + TOGGLE_FORM_MODE = "TOGGLE_FORM_MODE", + SET_RESET_CONFIRMATION_OPEN = "SET_RESET_CONFIRMATION_OPEN", + SET_SAVE_CONFIRMATION_OPEN = "SET_SAVE_CONFIRMATION_OPEN", + SET_DELETE_CONFIRMATION_OPEN = "SET_DELETE_CONFIRMATION_OPEN", + EXPORT_TASK_TO_JSON_FILE = "EXPORT_TASK_TO_JSON_FILE", + CONFIRM_RESET_TASK = "CONFIRM_RESET_TASK", +} + +export type ToggleFormModeEvent = { + type: TaskDefinitionFormEventType.TOGGLE_FORM_MODE; +}; + +export type SetSaveConfirmationEvent = { + type: TaskDefinitionFormEventType.SET_SAVE_CONFIRMATION_OPEN; +}; + +export type SetResetConfirmationEvent = { + type: TaskDefinitionFormEventType.SET_RESET_CONFIRMATION_OPEN; +}; + +export type SetDeleteConfirmationEvent = { + type: TaskDefinitionFormEventType.SET_DELETE_CONFIRMATION_OPEN; +}; + +export type ExportTaskToJsonEvent = { + type: TaskDefinitionFormEventType.EXPORT_TASK_TO_JSON_FILE; +}; + +export type ConfirmResetEvent = { + type: TaskDefinitionFormEventType.CONFIRM_RESET_TASK; +}; + +export type SetEditingFormFieldEvent = { + type: TaskDefinitionFormEventType.SET_EDITING_FORM_FIELD; + name: string; +}; + +export type HandleChangeTaskFormEvent = { + type: TaskDefinitionFormEventType.HANDLE_CHANGE_TASK_FORM; + name: string; + value: + | number + | string + | Record + | boolean + | null + | string[] + | undefined; +}; + +export type StopFormMachineEvent = { + type: TaskDefinitionFormEventType.STOP_FORM_MACHINE; +}; + +export type SyncDataToParentEvent = { + type: TaskDefinitionFormEventType.SYNC_DATA_TO_PARENT; +}; + +export type ResetFormEvent = { + type: TaskDefinitionFormEventType.RESET_FORM; +}; + +export type TaskDefinitionFormMachineEvent = + | HandleChangeTaskFormEvent + | SetEditingFormFieldEvent + | StopFormMachineEvent + | SyncDataToParentEvent + | ToggleFormModeEvent + | SetSaveConfirmationEvent + | SetResetConfirmationEvent + | SetDeleteConfirmationEvent + | ExportTaskToJsonEvent + | ConfirmResetEvent + | ResetFormEvent; diff --git a/ui-next/src/pages/definition/task/index.ts b/ui-next/src/pages/definition/task/index.ts new file mode 100644 index 0000000000..52cf5094e0 --- /dev/null +++ b/ui-next/src/pages/definition/task/index.ts @@ -0,0 +1,3 @@ +import TaskDefinition from "pages/definition/task/TaskDefinition"; + +export { TaskDefinition }; diff --git a/ui-next/src/pages/definition/task/state/actions.ts b/ui-next/src/pages/definition/task/state/actions.ts new file mode 100644 index 0000000000..41cdcb286b --- /dev/null +++ b/ui-next/src/pages/definition/task/state/actions.ts @@ -0,0 +1,202 @@ +import { TaskDefinitionFormContext } from "pages/definition/task/form/state"; +import { newTaskTemplate } from "templates/JSONSchemaWorkflow"; +import { TaskDefinitionDto } from "types/TaskDefinition"; +import { logger, randomChars } from "utils"; +import { assign, DoneInvokeEvent, send } from "xstate"; +import { cancel } from "xstate/lib/actions"; +import { + DebounceHandleChangeTaskDefinitionEvent, + HandleChangeTaskDefinitionEvent, + SetInputParametersEvent, + SetSaveConfirmationOpenEvent, + SetTaskDefinitionEvent, + SetTaskDomainEvent, + TaskDefinitionMachineContext, + TaskDefinitionMachineEventType, + TaskDefinitionMachineState, +} from "./types"; + +export const handleChangeTaskDefinition = assign< + TaskDefinitionMachineContext, + HandleChangeTaskDefinitionEvent +>(({ modifiedTaskDefinition, couldNotParseJson }, event) => { + let result = modifiedTaskDefinition; + let parsingResultWentWell = couldNotParseJson; + try { + result = JSON.parse(event.modifiedTaskDefinitionString); + parsingResultWentWell = true; + } catch { + logger.info("Json is broken"); + parsingResultWentWell = false; + } + + return { + modifiedTaskDefinitionString: event.modifiedTaskDefinitionString, + modifiedTaskDefinition: result, + couldNotParseJson: !parsingResultWentWell, + }; +}); + +export const debounceChangeTaskDefinition = send< + TaskDefinitionMachineContext, + DebounceHandleChangeTaskDefinitionEvent +>( + (__, { modifiedTaskDefinitionString }) => { + return { + type: TaskDefinitionMachineEventType.HANDLE_CHANGE_TASK_DEFINITION, + modifiedTaskDefinitionString, + }; + }, + { delay: 10, id: "debounceChangeTaskDefinition" }, +); + +export const cancelDebounceChangeTaskDefinition = cancel( + "debounceChangeTaskDefinition", +); + +export const persistTaskDefinitionByName = assign< + TaskDefinitionMachineContext, + DoneInvokeEvent +>((context, { data }) => { + const jsonString = JSON.stringify(data, null, 2); + + return { + originTaskDefinitionString: jsonString, // Not necesary + modifiedTaskDefinitionString: jsonString, // Not necesary + originTaskDefinition: data, + modifiedTaskDefinition: data, + }; +}); + +export const persistError = assign< + TaskDefinitionFormContext, + DoneInvokeEvent<{ error: { [key: string]: any }; numberOfError: number }> +>((context, { data }) => ({ + error: data.error, + numberOfError: data.numberOfError, +})); + +export const changeIsContinueCreate = assign< + TaskDefinitionMachineContext, + SetSaveConfirmationOpenEvent +>({ + isContinueCreate: (_, { isContinueCreate }) => isContinueCreate, +}); + +export const updateOriginTaskDefinition = assign< + TaskDefinitionMachineContext, + DoneInvokeEvent +>((context) => { + const newVersion = context.modifiedTaskDefinition; + const jsonString = JSON.stringify(newVersion, null, 2); + + return { + originTaskDefinition: newVersion, + originTaskDefinitionString: jsonString, // Not necesary + modifiedTaskDefinitionString: jsonString, // Not necesary + modifiedTaskDefinition: newVersion, + }; +}); + +// Maybe not needed +export const setIsEditTaskDef = assign(() => ({ + isNewTaskDef: false, +})); + +export const resetContext = assign( + ({ originTaskDefinition }) => { + const jsonString = JSON.stringify(originTaskDefinition, null, 2); + + return { + isContinueCreate: undefined, + originTaskDefinitionString: jsonString, + modifiedTaskDefinitionString: jsonString, + modifiedTaskDefinition: originTaskDefinition, + originTaskDefinition, + popoverMessage: null, + error: undefined, + numberOfError: undefined, + }; + }, +); + +export const prepareNewTaskContext = assign( + ({ user, authHeaders }) => { + const initTaskDefinition = { + ...newTaskTemplate(user?.id || "example@email.com"), + name: `task-${randomChars(6)}`, + } as Partial; + const jsonString = JSON.stringify(initTaskDefinition, null, 2); + + return { + authHeaders, + user, + bulkMode: false, + isContinueCreate: undefined, + isNewTaskDef: true, + originTaskDefinitionString: jsonString, + modifiedTaskDefinitionString: jsonString, + modifiedTaskDefinition: initTaskDefinition, + originTaskDefinition: initTaskDefinition, + popoverMessage: null, + error: undefined, + numberOfError: undefined, + }; + }, +); + +export const setInputParameters = assign< + TaskDefinitionMachineContext, + SetInputParametersEvent +>((_, { inputParameters }) => ({ + testInputParameters: inputParameters, +})); + +export const setTaskDomain = assign< + TaskDefinitionMachineContext, + SetTaskDomainEvent +>((_, { domain }) => ({ + testTaskDomain: domain, +})); + +export const persistWorkflowId = assign< + TaskDefinitionMachineContext, + DoneInvokeEvent +>({ + testTaskWorkflowId: (_context, { data }) => data, +}); + +export const syncDataFromFormMachine = assign< + TaskDefinitionMachineContext, + DoneInvokeEvent +>((_, { data }) => { + return { + modifiedTaskDefinition: data.modifiedTaskDefinition, + modifiedTaskDefinitionString: JSON.stringify( + data.modifiedTaskDefinition, + null, + 2, + ), + error: data.error, + numberOfError: data.numberOfError, + lastSelectedTab: TaskDefinitionMachineState.FORM, + }; +}); + +export const cleanLastSelectedTab = assign({ + lastSelectedTab: undefined, +}); + +export const setNameOnOriginTaskDefinition = assign< + TaskDefinitionFormContext, + SetTaskDefinitionEvent +>((context, { name, isNew }) => { + const taskDefintionDto: TaskDefinitionDto = { + ...context.modifiedTaskDefinition, + name, + }; + return { + originTaskDefinition: taskDefintionDto, + isNewTaskDef: isNew, + }; +}); diff --git a/ui-next/src/pages/definition/task/state/guards.ts b/ui-next/src/pages/definition/task/state/guards.ts new file mode 100644 index 0000000000..1e71d328ce --- /dev/null +++ b/ui-next/src/pages/definition/task/state/guards.ts @@ -0,0 +1,84 @@ +import { + CancelConfirmSaveEvent, + HandleChangeTaskDefinitionEvent, + HandleDefineNewConfirmationEvent, + HandleDeleteTaskDefConfirmationEvent, + HandleResetConfirmationEvent, + SaveTaskDefinitionEvent, + SetResetConfirmationOpenEvent, + TaskDefinitionMachineContext, + TaskDefinitionMachineEventType, + TaskDefinitionMachineState, +} from "pages/definition/task/state/types"; +import fastDeepEqual from "fast-deep-equal"; +import { DoneInvokeEvent, GuardMeta } from "xstate"; + +export const isChanged = ( + context: TaskDefinitionMachineContext, + event: HandleChangeTaskDefinitionEvent | SetResetConfirmationOpenEvent, +) => { + switch (event.type) { + case TaskDefinitionMachineEventType.HANDLE_CHANGE_TASK_DEFINITION: + return !fastDeepEqual( + context.originTaskDefinitionString, + event.modifiedTaskDefinitionString, + ); + + case TaskDefinitionMachineEventType.SET_RESET_CONFIRMATION_OPEN: + return !fastDeepEqual( + context.originTaskDefinition, + context.modifiedTaskDefinition, + ); + + default: + return false; + } +}; + +export const isNewTaskDef = (context: TaskDefinitionMachineContext) => + context.isNewTaskDef; + +export const isEditTaskDefinition = (context: TaskDefinitionMachineContext) => + !context.isNewTaskDef; + +export const isConfirm = ( + _: TaskDefinitionMachineContext, + event: + | HandleDefineNewConfirmationEvent + | HandleDeleteTaskDefConfirmationEvent + | HandleResetConfirmationEvent, +) => { + return event.isConfirm; +}; + +export const isSaveConfirmationOpen = ( + context: TaskDefinitionMachineContext, + event: HandleDefineNewConfirmationEvent, + { + state, + }: GuardMeta, +) => { + //@ts-ignore + return state.value?.dialog?.saveConfirmationOpen === "open"; +}; + +export const lastTabWasForm = (context: TaskDefinitionMachineContext) => + context.lastSelectedTab === TaskDefinitionMachineState.FORM; + +export const isFormModeHist = ( + context: TaskDefinitionMachineContext, + event: CancelConfirmSaveEvent | SaveTaskDefinitionEvent, + { + state, + }: GuardMeta< + TaskDefinitionMachineContext, + | HandleDefineNewConfirmationEvent + | SaveTaskDefinitionEvent + | DoneInvokeEvent + >, +) => { + return !!state.history?.matches("form"); +}; + +export const isContinueCreate = (context: TaskDefinitionMachineContext) => + context.isContinueCreate; diff --git a/ui-next/src/pages/definition/task/state/helpers.ts b/ui-next/src/pages/definition/task/state/helpers.ts new file mode 100644 index 0000000000..a69a7ba447 --- /dev/null +++ b/ui-next/src/pages/definition/task/state/helpers.ts @@ -0,0 +1,67 @@ +import type { ErrorObject } from "ajv"; +import _set from "lodash/set"; + +export const TASK_DEFINITION_SAVED_SUCCESSFULLY_MESSAGE = + "Task definition saved successfully."; + +export const TASK_FORM_MACHINE_ID = "taskDefinitionFormMachine"; +export const TASK_DIALOGS_MACHINE_ID = "taskDefinitionDialogsMachine"; + +/** + * Parse errors (array) to object + * @param errors + */ +export const parseErrors = (errors: ErrorObject[] | null) => + errors + ? errors.reduce( + (obj, { instancePath, schemaPath, params, keyword, message }) => { + const keys = instancePath.split("/") as string[]; + + if (keyword === "required" && params?.missingProperty) { + keys.push(params.missingProperty); + } + + // Remove the 1st empty ("") item in the array + keys.shift(); + + const errorKey = keys.at(-1); + + if (errorKey) { + if ( + !schemaPath.startsWith("#/") && + keys.length === 1 && + keyword === "type" + ) { + keys.push(keyword); + } + + return _set(obj, keys.join("."), { + message, + }); + } + + // Checking unique items in array (bulk mode) + if (keyword === "uniqueItems") { + return { + ...obj, + misc: { + uniqueItems: { message }, + }, + }; + } + + if (keyword) { + return _set( + obj, + params.type === "array" ? `misc.${keyword}` : keyword, + { + message, + }, + ); + } + + return obj; + }, + {}, + ) + : {}; diff --git a/ui-next/src/pages/definition/task/state/hook.ts b/ui-next/src/pages/definition/task/state/hook.ts new file mode 100644 index 0000000000..38a20ebde5 --- /dev/null +++ b/ui-next/src/pages/definition/task/state/hook.ts @@ -0,0 +1,321 @@ +import { useActor, useSelector } from "@xstate/react"; +import fastDeepEqual from "fast-deep-equal"; +import { useContext } from "react"; +import { ActorRef } from "xstate"; + +import { MessageContext } from "components/v1/layout/MessageContext"; +import { + TaskDefinitionMachineEvent, + TaskDefinitionMachineEventType, + TaskDefinitionMachineState, +} from "pages/definition/task/state/types"; +import { newTaskTemplate } from "templates/JSONSchemaWorkflow"; +import { PopoverMessage } from "types/Messages"; +import { TASK_DIALOGS_MACHINE_ID, TASK_FORM_MACHINE_ID } from "./helpers"; + +export const useTaskDefinition = ( + actor: ActorRef, +) => { + const { setMessage } = useContext(MessageContext); + // Use send from useActor but don't subscribe to state changes - use selectors instead + const [, send] = useActor(actor); + const modifiedTaskDefinition = useSelector( + actor, + (state) => state.context.modifiedTaskDefinition, + ); + + const originTaskDefinition = useSelector( + actor, + (state) => state.context.originTaskDefinition, + ); + + const originTaskDefinitionString = useSelector( + actor, + (state) => state.context.originTaskDefinitionString, + ); + + const modifiedTaskDefinitionString = useSelector( + actor, + (state) => state.context.modifiedTaskDefinitionString, + ); + + const taskDefinitions = useSelector( + actor, + (state) => state.context.taskDefinitions, + ); + + const originTaskDefinitions = useSelector( + actor, + (state) => state.context.originTaskDefinitions, + ); + + const isModified = useSelector(actor, (state) => + state.matches("editor.modified"), + ); + + const testInputParameters = useSelector( + actor, + (state) => state.context.testInputParameters, + ); + + const testTaskDomain = useSelector( + actor, + (state) => state.context.testTaskDomain, + ); + + const testTaskWorkflowId = useSelector( + actor, + (state) => state.context.testTaskWorkflowId, + ); + + const couldNotParseJson = useSelector( + actor, + (state) => state.context.couldNotParseJson, + ); + + const isDialogOpen = useSelector(actor, (state) => + state.matches("editor.dialog"), + ); + + const isFetching = useSelector(actor, (state) => + [ + "editor.fetchTaskDefinitions", + "editor.fetchTaskDefinitionByName", + "diffEditor.fetchTaskDefinitionByName", + "diffEditor.createTaskDefinition", + "diffEditor.updateTaskDefinition", + ].some(state.matches), + ); + + const isReady = useSelector(actor, (state) => + state.matches([TaskDefinitionMachineState.READY]), + ); + + const isContinueCreate = useSelector( + actor, + (state) => state.context.isContinueCreate, + ); + + const isNewTaskDef = useSelector( + actor, + (state) => state.context.isNewTaskDef, + ); + + const error = useSelector(actor, (state) => state.context.error); + + const numberOfError = useSelector( + actor, + (state) => state.context.numberOfError, + ); + + const isEditingName = useSelector(actor, (state) => + state.matches("ready.form.editingField.name"), + ); + + const isEditingDescription = useSelector(actor, (state) => + state.matches("ready.form.editingField.description"), + ); + + const isConfirmingSave = useSelector(actor, (state) => + state.matches([ + TaskDefinitionMachineState.READY, + TaskDefinitionMachineState.MAIN_CONTAINER, + TaskDefinitionMachineState.DIFF_EDITOR, + ]), + ); + + const isEditingInEditor = useSelector(actor, (state) => + state.matches([ + TaskDefinitionMachineState.READY, + TaskDefinitionMachineState.MAIN_CONTAINER, + TaskDefinitionMachineState.EDITOR, + ]), + ); + + const isEqual = useSelector( + actor, + ({ + context: { + bulkMode, + mode, + modifiedTaskDefinitionString, + originTaskDefinitionString, + originTaskDefinitions, + taskDefinitions, + }, + }) => { + const isBulkModeEqual = fastDeepEqual( + originTaskDefinitions, + taskDefinitions, + ); + const isJSONStringEqual = fastDeepEqual( + originTaskDefinitionString, + modifiedTaskDefinitionString, + ); + + if (mode === "editor") { + return bulkMode ? isBulkModeEqual : isJSONStringEqual; + } + + if (isConfirmingSave) { + return isJSONStringEqual; + } + + return false; + }, + ); + + // @ts-ignore + const formActor = actor?.children?.get(TASK_FORM_MACHINE_ID); + + // @ts-ignore + const dialogActor = actor?.children?.get(TASK_DIALOGS_MACHINE_ID); + + // FUNCTIONS + + const needSyncData = () => { + send({ + type: TaskDefinitionMachineEventType.NEED_SYNC_DATA_FROM_FORM_MACHINE, + }); + }; + + const handleChangeTaskDefinition = (editorValue: string) => { + send({ + type: TaskDefinitionMachineEventType.DEBOUNCE_HANDLE_CHANGE_TASK_DEFINITION, + modifiedTaskDefinitionString: editorValue, + }); + }; + + const handleRunTestTask = () => { + send({ + type: TaskDefinitionMachineEventType.HANDLE_RUN_TEST_TASK, + }); + }; + + const setInputParameters = (inputParameters: string) => { + send({ + type: TaskDefinitionMachineEventType.SET_INPUT_PARAMETERS, + inputParameters, + }); + }; + + const setTaskDomain = (domain: string) => { + send({ + type: TaskDefinitionMachineEventType.SET_TASK_DOMAIN, + domain, + }); + }; + + // Get additional values needed for convertJSONToString + const user = useSelector(actor, (state) => state.context.user); + const bulkMode = useSelector(actor, (state) => state.context.bulkMode); + + const convertJSONToString = () => { + if (isNewTaskDef) { + const initialDef = newTaskTemplate(user?.email || "example@email.com"); + + return JSON.stringify(bulkMode ? [initialDef] : initialDef, null, 2); + } + + return JSON.stringify(modifiedTaskDefinition, null, 2); + }; + + const handlePopoverMessage = (popoverMessage: PopoverMessage | null) => { + setMessage(popoverMessage); + }; + + const saveTaskDefinition = () => { + send({ + type: TaskDefinitionMachineEventType.SAVE_TASK_DEFINITION, + }); + }; + + const handleDownloadFile = () => { + send({ + type: TaskDefinitionMachineEventType.EXPORT_TASK_TO_JSON_FILE, + }); + }; + + const setSaveConfirmationOpen = (isContinueCreate = false) => { + send({ + type: TaskDefinitionMachineEventType.SET_SAVE_CONFIRMATION_OPEN, + isContinueCreate, + }); + }; + + const setResetConfirmationOpen = () => { + needSyncData(); + send({ + type: TaskDefinitionMachineEventType.SET_RESET_CONFIRMATION_OPEN, + }); + }; + + const setDeleteConfirmationOpen = () => { + send({ + type: TaskDefinitionMachineEventType.SET_DELETE_CONFIRMATION_OPEN, + }); + }; + + const cancelConfirmSave = () => { + send({ type: TaskDefinitionMachineEventType.CANCEL_CONFIRM_SAVE }); + }; + + const closeDialog = () => { + send({ type: TaskDefinitionMachineEventType.CLOSE_DIALOG }); + }; + + const toggleFormMode = (value: boolean) => { + send({ + type: TaskDefinitionMachineEventType.TOGGLE_FORM_MODE, + formMode: value, + }); + }; + + return [ + { + dialogActor, + error, + formActor, + isContinueCreate, + isDialogOpen, + isEditingDescription, + isEditingName, + isEqual, + isFetching, + isReady, + isModified, + isNewTaskDef, + modifiedTaskDefinition, + modifiedTaskDefinitionString, + numberOfError, + originTaskDefinition, + originTaskDefinitionString, + originTaskDefinitions, + saveConfirmationOpen: isConfirmingSave, + taskDefinitions, + testInputParameters, + testTaskDomain, + testTaskWorkflowId, + isEditingInEditor, + isConfirmingSave, + couldNotParseJson, + }, + { + cancelConfirmSave, + closeDialog, + convertJSONToString, + handleChangeTaskDefinition, + handleDownloadFile, + handlePopoverMessage, + handleRunTestTask, + needSyncData, + saveTaskDefinition, + setDeleteConfirmationOpen, + setInputParameters, + setResetConfirmationOpen, + setSaveConfirmationOpen, + setTaskDomain, + toggleFormMode, + }, + ] as const; +}; diff --git a/ui-next/src/pages/definition/task/state/index.ts b/ui-next/src/pages/definition/task/state/index.ts new file mode 100644 index 0000000000..79fd82215f --- /dev/null +++ b/ui-next/src/pages/definition/task/state/index.ts @@ -0,0 +1,2 @@ +export * from "./machine"; +export * from "./types"; diff --git a/ui-next/src/pages/definition/task/state/machine.ts b/ui-next/src/pages/definition/task/state/machine.ts new file mode 100644 index 0000000000..c60c657974 --- /dev/null +++ b/ui-next/src/pages/definition/task/state/machine.ts @@ -0,0 +1,362 @@ +import { createMachine, DoneInvokeEvent, forwardTo } from "xstate"; +import { + TaskDefinitionMachineContext, + TaskDefinitionMachineEvent, + TaskDefinitionMachineEventType, + TaskDefinitionMachineState, +} from "./types"; +import { TaskDefinitionDto } from "types/TaskDefinition"; +import * as customActions from "./actions"; +import * as guards from "./guards"; +import * as services from "./services"; +import { taskDefinitionFormMachine } from "pages/definition/task/form/state"; +import { TASK_FORM_MACHINE_ID } from "pages/definition/task/state/helpers"; + +export const taskDefinitionMachine = createMachine< + TaskDefinitionMachineContext, + TaskDefinitionMachineEvent +>( + { + id: "taskDefinitionMachine", + predictableActionArguments: true, + initial: TaskDefinitionMachineState.INIT, + context: { + authHeaders: {}, + isContinueCreate: false, + isNewTaskDef: true, + originTaskDefinitionString: "", + modifiedTaskDefinitionString: "", + modifiedTaskDefinition: {} as TaskDefinitionDto, + originTaskDefinition: {} as TaskDefinitionDto, + originTaskDefinitions: [] as TaskDefinitionDto[], + testInputParameters: "{}", + testTaskDomain: "", + couldNotParseJson: false, + lastSelectedTab: undefined, + }, + on: { + [TaskDefinitionMachineEventType.SET_TASK_DEFINITION]: { + actions: ["setNameOnOriginTaskDefinition"], + target: TaskDefinitionMachineState.INIT, + }, + }, + states: { + [TaskDefinitionMachineState.INIT]: { + always: [ + { + cond: "isEditTaskDefinition", + target: TaskDefinitionMachineState.FETCH_FOR_TASK_DEFINITION, + }, + { target: TaskDefinitionMachineState.READY }, + ], + }, + [TaskDefinitionMachineState.FETCH_FOR_TASK_DEFINITION]: { + invoke: { + src: "fetchTaskDefinitionByNameService", + onDone: { + target: TaskDefinitionMachineState.READY, + actions: ["persistTaskDefinitionByName"], + }, + onError: { + target: TaskDefinitionMachineState.FINISH, + actions: ["setErrorMessage"], + }, + }, + }, + [TaskDefinitionMachineState.READY]: { + type: "parallel", + states: { + [TaskDefinitionMachineState.MAIN_CONTAINER]: { + initial: TaskDefinitionMachineState.FORM, + states: { + [TaskDefinitionMachineState.FORM]: { + initial: "idle", + states: { + idle: { + on: { + [TaskDefinitionMachineEventType.TOGGLE_FORM_MODE]: { + actions: forwardTo(TASK_FORM_MACHINE_ID), + }, + [TaskDefinitionMachineEventType.CONFIRM_RESET_TASK]: { + actions: forwardTo(TASK_FORM_MACHINE_ID), + }, + [TaskDefinitionMachineEventType.SET_SAVE_CONFIRMATION_OPEN]: + { + actions: [ + forwardTo(TASK_FORM_MACHINE_ID), + "changeIsContinueCreate", + ], + }, + // The form handles this event. since it has the last version + [TaskDefinitionMachineEventType.EXPORT_TASK_TO_JSON_FILE]: + { + actions: forwardTo(TASK_FORM_MACHINE_ID), + }, + }, + invoke: { + src: taskDefinitionFormMachine, + id: TASK_FORM_MACHINE_ID, + data: ({ + modifiedTaskDefinition, + originTaskDefinition, + isNewTaskDef, + }) => ({ + modifiedTaskDefinition, + originTaskDefinition, + isNewTaskDef, + }), + onDone: [ + { + target: `#taskDefinitionMachine.${TaskDefinitionMachineState.READY}.${TaskDefinitionMachineState.MAIN_CONTAINER}.${TaskDefinitionMachineState.EDITOR}`, + cond: ( + __context: TaskDefinitionMachineContext, + event: DoneInvokeEvent<{ + reason: TaskDefinitionMachineEventType; + }>, + ) => { + return ( + event.data.reason === + TaskDefinitionMachineEventType.TOGGLE_FORM_MODE + ); + }, + actions: ["syncDataFromFormMachine"], + } as any, + { + target: `#taskDefinitionMachine.${TaskDefinitionMachineState.READY}.${TaskDefinitionMachineState.MAIN_CONTAINER}.${TaskDefinitionMachineState.DIFF_EDITOR}`, + cond: ( + __context: TaskDefinitionMachineContext, + event: DoneInvokeEvent<{ + reason: TaskDefinitionMachineEventType; + }>, + ) => { + return ( + event.data.reason === + TaskDefinitionMachineEventType.SET_SAVE_CONFIRMATION_OPEN + ); + }, + actions: ["syncDataFromFormMachine"], + }, + { target: "idle" }, + ], + }, + }, + }, + }, + [TaskDefinitionMachineState.EDITOR]: { + entry: "cleanLastSelectedTab", // Note if last selected tab is undefined it will go to the form + on: { + [TaskDefinitionMachineEventType.HANDLE_CHANGE_TASK_DEFINITION]: + [ + { + actions: ["handleChangeTaskDefinition"], + }, + ], + [TaskDefinitionMachineEventType.DEBOUNCE_HANDLE_CHANGE_TASK_DEFINITION]: + { + actions: [ + "cancelDebounceChangeTaskDefinition", + "debounceChangeTaskDefinition", + ], + }, + [TaskDefinitionMachineEventType.TOGGLE_FORM_MODE]: { + target: TaskDefinitionMachineState.FORM, + }, + [TaskDefinitionMachineEventType.SET_SAVE_CONFIRMATION_OPEN]: { + actions: ["changeIsContinueCreate"], + target: TaskDefinitionMachineState.DIFF_EDITOR, + }, + }, + initial: "idle", + states: { + idle: { + on: { + [TaskDefinitionMachineEventType.EXPORT_TASK_TO_JSON_FILE]: + { + target: "exportTask", + }, + }, + }, + exportTask: { + invoke: { + src: "handleDownloadFile", + onDone: { + target: "idle", + }, + onError: { + actions: ["setErrorMessage"], + }, + }, + }, + }, + }, + [TaskDefinitionMachineState.DIFF_EDITOR]: { + initial: "idle", + states: { + idle: { + on: { + [TaskDefinitionMachineEventType.HANDLE_CHANGE_TASK_DEFINITION]: + [ + { + actions: ["handleChangeTaskDefinition"], + }, + ], + [TaskDefinitionMachineEventType.DEBOUNCE_HANDLE_CHANGE_TASK_DEFINITION]: + { + actions: [ + "cancelDebounceChangeTaskDefinition", + "debounceChangeTaskDefinition", + ], + }, + [TaskDefinitionMachineEventType.CANCEL_CONFIRM_SAVE]: { + target: `#taskDefinitionMachine.${TaskDefinitionMachineState.READY}.${TaskDefinitionMachineState.MAIN_CONTAINER}.${TaskDefinitionMachineState.EDITOR}`, // not really editor though should return to previous tab + }, + [TaskDefinitionMachineEventType.SAVE_TASK_DEFINITION]: { + target: "createTaskDefinition", + }, + }, + }, + createTaskDefinition: { + invoke: { + src: "createOrUpdateTaskDefinitionService", + onDone: [ + { + cond: "isContinueCreate", + target: `#taskDefinitionMachine.${TaskDefinitionMachineState.INIT}`, + + actions: [ + "showSaveSuccessMessage", + "prepareNewTaskContext", + "redirectToNewTask", + ], + }, + { + target: `#taskDefinitionMachine.${TaskDefinitionMachineState.INIT}`, + actions: [ + "updateOriginTaskDefinition", + "showSaveSuccessMessage", + "setIsEditTaskDef", + "redirectToEditTask", + ], + }, + ], + onError: [ + { + cond: "lastTabWasForm", + target: `#taskDefinitionMachine.${TaskDefinitionMachineState.READY}.${TaskDefinitionMachineState.MAIN_CONTAINER}.${TaskDefinitionMachineState.FORM}`, + actions: ["setErrorMessage", "cleanLastSelectedTab"], + }, + { + target: `#taskDefinitionMachine.${TaskDefinitionMachineState.READY}.${TaskDefinitionMachineState.MAIN_CONTAINER}.${TaskDefinitionMachineState.EDITOR}`, + actions: ["setErrorMessage", "cleanLastSelectedTab"], + }, + ], + }, + }, + }, + }, + // Move to parallel state + }, + }, + [TaskDefinitionMachineState.TASK_TESTER]: { + initial: "idle", + states: { + idle: { + on: { + [TaskDefinitionMachineEventType.SET_INPUT_PARAMETERS]: { + actions: ["setInputParameters"], + }, + [TaskDefinitionMachineEventType.SET_TASK_DOMAIN]: { + actions: ["setTaskDomain"], + }, + [TaskDefinitionMachineEventType.HANDLE_RUN_TEST_TASK]: { + target: "runTestTask", + }, + }, + }, + runTestTask: { + invoke: { + src: "runTestTaskService", + onDone: { + actions: ["persistWorkflowId"], + target: "idle", + }, + onError: { + actions: ["setErrorMessage"], + target: "idle", + }, + }, + }, + }, + }, + [TaskDefinitionMachineState.RESET_FORM]: { + initial: "idle", + states: { + idle: { + on: { + [TaskDefinitionMachineEventType.SET_RESET_CONFIRMATION_OPEN]: + { + target: TaskDefinitionMachineState.RESET_FORM_CONFIRM, + }, + }, + }, + [TaskDefinitionMachineState.RESET_FORM_CONFIRM]: { + on: { + [TaskDefinitionMachineEventType.CONFIRM_RESET_TASK]: { + actions: ["resetContext"], + target: "idle", + }, + [TaskDefinitionMachineEventType.CANCEL_CONFIRM_SAVE]: { + target: "idle", + }, + }, + }, + }, + }, + [TaskDefinitionMachineState.DELETE_FORM]: { + initial: "idle", + states: { + idle: { + on: { + [TaskDefinitionMachineEventType.SET_DELETE_CONFIRMATION_OPEN]: + { + target: TaskDefinitionMachineState.DELETE_FORM_CONFIRM, + }, + }, + }, + [TaskDefinitionMachineState.DELETE_FORM_CONFIRM]: { + on: { + [TaskDefinitionMachineEventType.CONFIRM_RESET_TASK]: { + target: "deleteTaskDefinition", + }, + [TaskDefinitionMachineEventType.CANCEL_CONFIRM_SAVE]: { + target: "idle", + }, + }, + }, + deleteTaskDefinition: { + invoke: { + src: "deleteTaskDefinitionService", + onDone: { + actions: ["redirectToTaskList"], + }, + onError: { + target: `#taskDefinitionMachine.${TaskDefinitionMachineState.READY}`, + actions: ["setErrorMessage"], + }, + }, + }, + }, + }, + }, + }, + [TaskDefinitionMachineState.FINISH]: { + type: "final", + }, + }, + }, + { + actions: customActions as any, + guards: guards as any, + services: services as any, + }, +); diff --git a/ui-next/src/pages/definition/task/state/services.ts b/ui-next/src/pages/definition/task/state/services.ts new file mode 100644 index 0000000000..7837f0e5ca --- /dev/null +++ b/ui-next/src/pages/definition/task/state/services.ts @@ -0,0 +1,268 @@ +import { queryClient } from "queryClient"; +import { fetchWithContext, fetchContextNonHook } from "plugins/fetch"; +import { TaskDefinitionMachineContext } from "pages/definition/task/state/types"; +import { + exportObjToFile, + getErrors, + tryToJson, + featureFlags, + FEATURES, + tryFunc, + logger, +} from "utils"; +import { TaskDefinitionDto } from "types/TaskDefinition"; +import { ErrorObj } from "types/common"; + +const taskVisibility = featureFlags.getValue(FEATURES.TASK_VISIBILITY, "READ"); +const fetchContext = fetchContextNonHook(); + +export const fetchTaskDefinitionsService = async ({ + authHeaders: headers, +}: TaskDefinitionMachineContext) => { + const taskDefinitionsPath = `/metadata/taskdefs?access=${taskVisibility}`; + + try { + const response = await queryClient.fetchQuery( + [fetchContext.stack, taskDefinitionsPath], + () => fetchWithContext(taskDefinitionsPath, fetchContext, { headers }), + ); + return response; + } catch (error) { + const errorDetail = await getErrors(error as Response); + + return Promise.reject({ + message: errorDetail.message + ? errorDetail.message + : "Fetching task definitions failed!", + }); + } +}; + +export const fetchTaskDefinitionByNameService = async ({ + authHeaders: headers, + originTaskDefinition, +}: TaskDefinitionMachineContext) => { + const taskDefinitionPath = `/metadata/taskdefs/${originTaskDefinition.name}`; + + return tryFunc({ + fn: async () => { + return await queryClient.fetchQuery( + [fetchContext.stack, taskDefinitionPath], + () => fetchWithContext(taskDefinitionPath, fetchContext, { headers }), + ); + }, + customError: { + message: "Fetching task definition by name failed!", + }, + showCustomError: false, + }); +}; + +const validateTaskName = (taskName?: string) => { + if (taskName?.includes(":")) { + return Promise.reject({ + message: 'Task name should not include the colon character ":"', + }); + } + return null; +}; + +export const createTaskDefinitionService = async ({ + authHeaders, + modifiedTaskDefinition, +}: TaskDefinitionMachineContext) => { + const validationError = validateTaskName(modifiedTaskDefinition.name); + + if (validationError) { + return validationError; + } + + const stringDefinition = JSON.stringify(modifiedTaskDefinition, null, 2); + const body = `[${stringDefinition}]`; + + return tryFunc({ + fn: async () => { + return await fetchWithContext( + "/metadata/taskdefs", + {}, + { + method: "POST", + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + body, + }, + ); + }, + customError: { + message: "Create a new task fail!", + }, + showCustomError: false, + }); +}; + +export const updateTaskDefinitionService = async ({ + authHeaders, + modifiedTaskDefinition, +}: TaskDefinitionMachineContext) => { + const validationError = validateTaskName(modifiedTaskDefinition.name); + + if (validationError) { + return validationError; + } + + const stringDefinition = JSON.stringify(modifiedTaskDefinition, null, 2); + + return tryFunc({ + fn: async () => { + return await fetchWithContext( + "/metadata/taskdefs", + {}, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + body: stringDefinition, + }, + ); + }, + customError: { + message: "Update task failed!", + }, + showCustomError: false, + }); +}; + +export const createOrUpdateTaskDefinitionService = async ( + context: TaskDefinitionMachineContext, +) => { + const taskChangedName = + context.modifiedTaskDefinition.name !== context.originTaskDefinition.name; + return context.isNewTaskDef || taskChangedName + ? createTaskDefinitionService(context) + : updateTaskDefinitionService(context); +}; + +export const deleteTaskDefinitionService = async ({ + authHeaders, + originTaskDefinition, +}: TaskDefinitionMachineContext) => { + if (!originTaskDefinition.name) { + return Promise.reject({ message: "Task's name is undefined" }); + } + + const taskDefinitionPath = `/metadata/taskdefs/${encodeURIComponent( + originTaskDefinition.name, + )}`; + + return tryFunc({ + fn: async () => { + return await fetchWithContext( + taskDefinitionPath, + {}, + { + method: "DELETE", + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + }, + ); + }, + customError: { + message: "Delete task failed!", + }, + showCustomError: false, + }); +}; + +export const runTestTaskService = async ({ + authHeaders, + modifiedTaskDefinition, + user, + testTaskDomain, + testInputParameters, +}: TaskDefinitionMachineContext) => { + // generate random string of six characters + const suffix = Math.random().toString(36).substring(2, 7); + if (modifiedTaskDefinition?.name == null) { + logger.error("Task name is null"); + return Promise.reject({ + message: "Task name is null", + }); + } + + const workflowWithTask = { + name: `test_task_{${modifiedTaskDefinition.name}}_${suffix}_wf`, + version: 1, + workflowDef: { + name: `TestTask_${modifiedTaskDefinition.name}_${suffix}`, + description: `Dynamic workflow to test the task: [${modifiedTaskDefinition.name}]`, + version: 1, + tasks: [ + { + name: modifiedTaskDefinition.name, + taskReferenceName: `test_task_${modifiedTaskDefinition.name}_${suffix}`, + type: "SIMPLE", + inputParameters: + testInputParameters && tryToJson(testInputParameters), + }, + ], + createdBy: user?.id || "example@email.com", + }, + ...(testTaskDomain + ? { + taskToDomain: { + [modifiedTaskDefinition.name]: testTaskDomain, + }, + } + : {}), + }; + + const body = JSON.stringify(workflowWithTask, null, 0); + + return tryFunc({ + fn: async () => { + return await fetchWithContext( + "/workflow", + {}, + { + method: "POST", + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + body, + }, + true, + ); + }, + customError: { + message: "Run test task failed.", + }, + showCustomError: false, + }); +}; + +export const handleDownloadFile = async ({ + modifiedTaskDefinition, +}: TaskDefinitionMachineContext) => { + try { + exportObjToFile({ + data: modifiedTaskDefinition, + fileName: `${modifiedTaskDefinition.name || "new"}.json`, + type: `application/json`, + }); + } catch (error: any) { + const errorDetail = await getErrors(error as Response); + + return Promise.reject({ + message: errorDetail.message + ? errorDetail.message + : "Download task failed!", + }); + } +}; diff --git a/ui-next/src/pages/definition/task/state/types.ts b/ui-next/src/pages/definition/task/state/types.ts new file mode 100644 index 0000000000..95a4c988df --- /dev/null +++ b/ui-next/src/pages/definition/task/state/types.ts @@ -0,0 +1,259 @@ +import { ActorRef } from "xstate"; + +import { AuthHeaders } from "types"; +import { PopoverMessage, TaskDefinitionDto } from "types"; +import { User } from "types/User"; +import { + TaskDefinitionFormContext, + TaskDefinitionFormMachineEvent, +} from "../form/state/types"; + +export enum TaskTimeoutPolicy { + RETRY = "RETRY", + TIME_OUT_WF = "TIME_OUT_WF", + ALERT_ONLY = "ALERT_ONLY", +} + +export const TaskTimeoutPolicyLabel = { + RETRY: "Retry Task", + TIME_OUT_WF: "Timeout Workflow", + ALERT_ONLY: "Alert Only", +} as { [key: string]: string }; + +export enum TaskRetryLogic { + FIXED = "FIXED", + LINEAR_BACKOFF = "LINEAR_BACKOFF", + EXPONENTIAL_BACKOFF = "EXPONENTIAL_BACKOFF", +} + +export const TaskRetryLogicLabel = { + FIXED: "Fixed", + LINEAR_BACKOFF: "Linear Backoff", + EXPONENTIAL_BACKOFF: "Exponential Backoff", +} as { [key: string]: string }; + +export interface TaskDefinitionButtonsProps { + taskDefActor: ActorRef; + showTestTask?: () => void; +} + +export interface TaskDefinitionDiffEditorProps { + taskDefActor: ActorRef; +} + +export interface TaskDefinitionFormProps { + formActor: ActorRef; +} + +export enum TaskDefinitionMachineState { + INIT = "init", + FORM = "form", + EDITOR = "editor", + RESET_FORM = "resteForm", + RESET_FORM_CONFIRM = "resetFormConfirm", + DELETE_FORM = "deleteForm", + DELETE_FORM_CONFIRM = "deleteFormConfirm", + DOWNLOAD_TASK_JSON = "downloadTaskJson", + MAIN_CONTAINER = "mainContainer", + TASK_TESTER = "taskTester", + DIFF_EDITOR = "diffEditor", + DIFF_EDITOR_CONFIRM = "diffEditorConfirm", + FINISH = "finish", + READY = "ready", + FETCH_FOR_TASK_DEFINITION = "fetchForTaskDefinition", +} + +export interface TaskDefinitionMachineContext { + authHeaders: AuthHeaders; + error?: { [key: string]: any }; + isContinueCreate?: boolean; + isNewTaskDef: boolean; + modifiedTaskDefinitionString: string; + originTaskDefinitionString: string; + modifiedTaskDefinition: Partial; + originTaskDefinition: Partial; + originTaskDefinitions: Partial[]; + couldNotParseJson: boolean; + user?: User; + testInputParameters?: string; + testTaskDomain?: string; + testTaskWorkflowId?: string; + lastSelectedTab?: + | TaskDefinitionMachineState.FORM + | TaskDefinitionMachineState.EDITOR; +} + +export enum TaskDefinitionMachineEventType { + CANCEL_CONFIRM_SAVE = "CANCEL_CONFIRM_SAVE", + CLOSE_DIALOG = "CLOSE_DIALOG", + DEBOUNCE_HANDLE_CHANGE_TASK_DEFINITION = "DEBOUNCE_HANDLE_CHANGE_TASK_DEFINITION", + HANDLE_CHANGE_TASK_DEFINITION = "HANDLE_CHANGE_TASK_DEFINITION", + HANDLE_DEFINE_NEW_CONFIRMATION = "HANDLE_DEFINE_NEW_CONFIRMATION", + SET_DEFINE_NEW_TASK_OPEN = "SET_DEFINE_NEW_TASK_OPEN", + HANDLE_DELETE_TASK_DEF_CONFIRMATION = "HANDLE_DELETE_TASK_DEF_CONFIRMATION", + HANDLE_RESET_CONFIRMATION = "HANDLE_RESET_CONFIRMATION", + HANDLE_RUN_TEST_TASK = "HANDLE_RUN_TEST_TASK", + NEW_SAVE_COMPLETE = "NEW_SAVE_COMPLETE", + SAVE_TASK_DEFINITION = "SAVE_TASK_DEFINITION", + SET_DELETE_CONFIRMATION_OPEN = "SET_DELETE_CONFIRMATION_OPEN", + SET_INPUT_PARAMETERS = "SET_INPUT_PARAMETERS", + SET_RESET_CONFIRMATION_OPEN = "SET_RESET_CONFIRMATION_OPEN", + SET_SAVE_CONFIRMATION_OPEN = "SET_SAVE_CONFIRMATION_OPEN", + SET_TASK_DOMAIN = "SET_TASK_DOMAIN", + TOGGLE_BULK_MODE = "TOGGLE_BULK_MODE", + TOGGLE_FORM_MODE = "TOGGLE_FORM_MODE", + SYNC_DATA_FROM_FORM_MACHINE = "SYNC_DATA_FROM_FORM_MACHINE", + NEED_SYNC_DATA_FROM_FORM_MACHINE = "NEED_SYNC_DATA_FROM_FORM_MACHINE", + EXPORT_TASK_TO_JSON_FILE = "EXPORT_TASK_TO_JSON_FILE", + CONFIRM_DELETE_TASK = "CONFIRM_DELETE_TASK", + CONFIRM_GO_TO_DEFINE_NEW_TASK = "CONFIRM_GO_TO_DEFINE_NEW_TASK", + CONFIRM_RESET_TASK = "CONFIRM_RESET_TASK", + SET_TASK_DEFINITION = "SET_TASK_DEFINITION", +} + +export type NewSaveCompleteEvent = { + isContinueCreate: boolean; + isNewTaskDef: boolean; + modifiedTaskDefinition: TaskDefinitionDto; + popoverMessage: PopoverMessage; + saveComplete: boolean; + saveConfirmationOpen: boolean; + taskIsModified: boolean; + type: TaskDefinitionMachineEventType.NEW_SAVE_COMPLETE; +}; + +export type HandleChangeTaskDefinitionEvent = { + type: TaskDefinitionMachineEventType.HANDLE_CHANGE_TASK_DEFINITION; + modifiedTaskDefinitionString: string; +}; + +export type DebounceHandleChangeTaskDefinitionEvent = { + type: TaskDefinitionMachineEventType.DEBOUNCE_HANDLE_CHANGE_TASK_DEFINITION; + modifiedTaskDefinitionString: string; +}; + +export type HandleResetConfirmationEvent = { + type: TaskDefinitionMachineEventType.HANDLE_RESET_CONFIRMATION; + isConfirm: boolean; +}; + +export type HandleDefineNewConfirmationEvent = { + type: TaskDefinitionMachineEventType.HANDLE_DEFINE_NEW_CONFIRMATION; + isConfirm: boolean; +}; + +export type HandleDeleteTaskDefConfirmationEvent = { + type: TaskDefinitionMachineEventType.HANDLE_DELETE_TASK_DEF_CONFIRMATION; + isConfirm: boolean; +}; + +export type HandleRunTestTaskEvent = { + type: TaskDefinitionMachineEventType.HANDLE_RUN_TEST_TASK; +}; + +export type HandleDefineNewTaskEvent = { + type: TaskDefinitionMachineEventType.SET_DEFINE_NEW_TASK_OPEN; +}; + +export type SaveTaskDefinitionEvent = { + type: TaskDefinitionMachineEventType.SAVE_TASK_DEFINITION; +}; + +export type SetSaveConfirmationOpenEvent = { + type: TaskDefinitionMachineEventType.SET_SAVE_CONFIRMATION_OPEN; + isContinueCreate: boolean; +}; + +export type SetDeleteConfirmationOpenEvent = { + type: TaskDefinitionMachineEventType.SET_DELETE_CONFIRMATION_OPEN; +}; + +export type SetResetConfirmationOpenEvent = { + type: TaskDefinitionMachineEventType.SET_RESET_CONFIRMATION_OPEN; +}; + +export type CancelConfirmSaveEvent = { + type: TaskDefinitionMachineEventType.CANCEL_CONFIRM_SAVE; +}; + +export type CloseDialogEvent = { + type: TaskDefinitionMachineEventType.CLOSE_DIALOG; +}; + +export type ToggleBulkModeEvent = { + type: TaskDefinitionMachineEventType.TOGGLE_BULK_MODE; + bulkMode: boolean; +}; + +export type SetInputParametersEvent = { + type: TaskDefinitionMachineEventType.SET_INPUT_PARAMETERS; + inputParameters: string; +}; + +export type SetTaskDomainEvent = { + type: TaskDefinitionMachineEventType.SET_TASK_DOMAIN; + domain: string; +}; + +export type ToggleFormModeEvent = { + type: TaskDefinitionMachineEventType.TOGGLE_FORM_MODE; + formMode: boolean; +}; + +export type SyncDataFromFormMachine = { + type: TaskDefinitionMachineEventType.SYNC_DATA_FROM_FORM_MACHINE; + data: TaskDefinitionFormContext; +}; + +export type NeedSyncDataFromFormMachineEvent = { + type: TaskDefinitionMachineEventType.NEED_SYNC_DATA_FROM_FORM_MACHINE; +}; + +export type ExportTaskToJSONFileEvent = { + type: TaskDefinitionMachineEventType.EXPORT_TASK_TO_JSON_FILE; +}; + +export type ConfirmDeleteTaskEvent = { + type: TaskDefinitionMachineEventType.CONFIRM_DELETE_TASK; +}; + +export type ConfirmGoToDefineNewTask = { + type: TaskDefinitionMachineEventType.CONFIRM_GO_TO_DEFINE_NEW_TASK; +}; + +export type ConfirmResetTaskEvent = { + type: TaskDefinitionMachineEventType.CONFIRM_RESET_TASK; +}; + +export type SetTaskDefinitionEvent = { + type: TaskDefinitionMachineEventType.SET_TASK_DEFINITION; + name: string; + isNew: boolean; +}; + +export type TaskDefinitionMachineEvent = + | CancelConfirmSaveEvent + | CloseDialogEvent + | ConfirmDeleteTaskEvent + | ConfirmGoToDefineNewTask + | ConfirmResetTaskEvent + | DebounceHandleChangeTaskDefinitionEvent + | ExportTaskToJSONFileEvent + | HandleChangeTaskDefinitionEvent + | HandleDefineNewConfirmationEvent + | HandleDefineNewTaskEvent + | HandleDeleteTaskDefConfirmationEvent + | HandleResetConfirmationEvent + | HandleRunTestTaskEvent + | NeedSyncDataFromFormMachineEvent + | NewSaveCompleteEvent + | SaveTaskDefinitionEvent + | SetDeleteConfirmationOpenEvent + | SetInputParametersEvent + | SetResetConfirmationOpenEvent + | SetSaveConfirmationOpenEvent + | SetTaskDomainEvent + | SyncDataFromFormMachine + | ToggleBulkModeEvent + | SetTaskDefinitionEvent + | ToggleFormModeEvent; diff --git a/ui-next/src/pages/definition/task/state/validator.ts b/ui-next/src/pages/definition/task/state/validator.ts new file mode 100644 index 0000000000..38eaf80dfe --- /dev/null +++ b/ui-next/src/pages/definition/task/state/validator.ts @@ -0,0 +1,147 @@ +import type { ErrorObject } from "ajv"; +import Ajv from "ajv"; +import ajvErrors from "ajv-errors"; +import { parseErrors } from "pages/definition/task/state/helpers"; +import { + TaskRetryLogic, + TaskTimeoutPolicy, +} from "pages/definition/task/state/types"; +import { TaskDefinitionDto } from "types/TaskDefinition"; +import { getErrors } from "utils/utils"; + +const taskSchema = { + $id: "/properties/task", + type: "object", + properties: { + name: { + type: "string", + pattern: "^[\\w-]+$", + errorMessage: { + pattern: + "Don't allow special characters. Normal characters, numbers and hyphens are allowed.", + }, + }, + description: { + type: "string", + }, + retryCount: { + type: "integer", + }, + timeoutSeconds: { + type: "integer", + }, + timeoutPolicy: { + type: "string", + enum: Object.values(TaskTimeoutPolicy), + errorMessage: { + type: "must be string.", + enum: `must be one of allowed values: ${Object.values( + TaskTimeoutPolicy, + ).join(" | ")}.`, + }, + }, + retryLogic: { + type: "string", + enum: Object.values(TaskRetryLogic), + errorMessage: { + type: "must be string.", + enum: `must be one of allowed values: ${Object.values( + TaskRetryLogic, + ).join(" | ")}.`, + }, + }, + retryDelaySeconds: { + type: "integer", + }, + responseTimeoutSeconds: { + type: "integer", + }, + rateLimitPerFrequency: { + type: "integer", + }, + rateLimitFrequencyInSeconds: { + type: "integer", + }, + ownerEmail: { + type: "string", + }, + pollTimeoutSeconds: { + type: "integer", + }, + concurrentExecLimit: { + type: "integer", + }, + backoffScaleFactor: { + type: "integer", + }, + inputKeys: { + type: "array", + items: { + type: "string", + }, + }, + outputKeys: { + type: "array", + items: { + type: "string", + }, + }, + inputTemplate: { + type: "object", + }, + }, + required: ["name"], +}; + +const tasksSchema = { + $id: "/properties/tasks", + type: "array", + uniqueItems: true, + items: { + $ref: taskSchema.$id, + }, +}; + +const ajv = new Ajv({ + schemas: [taskSchema, tasksSchema], + allErrors: true, +}); + +// Ajv option allErrors is required +ajvErrors(ajv /*, {singleError: true} */); + +export const validateTask = ( + task: TaskDefinitionDto | TaskDefinitionDto[], + isBulk: boolean, +): null | ErrorObject[] => { + const validate = ajv.compile(isBulk ? tasksSchema : taskSchema); + const valid = validate(task); + + if (!valid) { + return validate.errors as ErrorObject[]; + } + + return null; +}; + +export const validatingService = async ( + modifiedTaskDefinition: TaskDefinitionDto | TaskDefinitionDto[], + isBulk: boolean, +) => { + try { + const errors = validateTask(modifiedTaskDefinition, isBulk); + + return { + error: parseErrors(errors), + numberOfError: errors?.length || 0, + }; + } catch (error: any) { + const errorDetail = await getErrors(error as Response); + + return Promise.reject({ + message: errorDetail.message + ? errorDetail.message + : "Validate task failed!", + }); + } +}; diff --git a/ui-next/src/pages/definitions/EventHandler.tsx b/ui-next/src/pages/definitions/EventHandler.tsx new file mode 100644 index 0000000000..9f91bd239f --- /dev/null +++ b/ui-next/src/pages/definitions/EventHandler.tsx @@ -0,0 +1,473 @@ +import { + FormControl, + FormControlLabel, + Grid, + Radio, + RadioGroup, + Tooltip, +} from "@mui/material"; +import { Box } from "@mui/system"; +import { + Trash as DeleteIcon, + PauseCircle as PauseIcon, + ArrowClockwise as RefreshIcon, + TagIcon, +} from "@phosphor-icons/react"; +import { Button, DataTable, IconButton, NavLink } from "components"; +import NoDataComponent from "components/NoDataComponent"; +import Paper from "components/Paper"; +import { SnackbarMessage } from "components/SnackbarMessage"; +import TagChip from "components/TagChip"; +import ConfirmChoiceDialog from "components/ConfirmChoiceDialog"; +import AddTagDialog, { TagDialogProps } from "components/tags/AddTagDialog"; +import ConductorInput from "components/v1/ConductorInput"; +import { TagsRenderer } from "components/v1/TagList"; +import AddIcon from "components/v1/icons/AddIcon"; +import PlayIcon from "components/v1/icons/PlayIcon"; +import { useCallback, useMemo, useState } from "react"; +import { Helmet } from "react-helmet"; +import { useQueryState } from "react-router-use-location-state"; +import SectionContainer from "shared/SectionContainer"; +import SectionHeader from "shared/SectionHeader"; +import SectionHeaderActions from "shared/SectionHeaderActions"; +import { useAuth } from "shared/auth"; +import { colors } from "theme/tokens/variables"; +import { ConductorEvent } from "types/Events"; +import { TagDto } from "types/Tag"; +import { createSearchableTags, logger } from "utils"; +import { ACTIVE_FILTER_QUERY_PARAM } from "utils/constants/common"; +import { EVENT_HANDLERS_URL } from "utils/constants/route"; +import useCustomPagination from "utils/hooks/useCustomPagination"; +import { usePushHistory } from "utils/hooks/usePushHistory"; +import { useActionWithPath, useFetch } from "utils/query"; +import Header from "../../components/Header"; +import { + activeFilterGroups, + conditionalRowStyles, + getLinkColor, +} from "./rowColorHelpers"; + +const getStatusLabel = (status: boolean) => (status ? "Active" : "Inactive"); + +const INTRO_CONTENT = `Event handlers help you automate workflow responses to external events. Create handlers to trigger workflows when events occur from sources like Kafka, SQS, or custom events. Perfect for building event-driven architectures and real-time integrations. + +Read more: + +* [Developer Guides: Event Handlers](https://orkes.io/content/developer-guides/event-handler) +* [Eventing](https://orkes.io/content/eventing) +`; + +export default function EventDefinitionList() { + const { data: eventHandlers = [], isFetching, refetch } = useFetch("/event"); + const { isTrialExpired } = useAuth(); + const [toast, setToast] = useState({ + isOpen: false, + message: "", + status: "", + }); + const [showAddTagDialog, setShowAddTagDialog] = useState(false); + const [addTagDialogData, setAddTagDialogData] = + useState(null); + + const pushHistory = usePushHistory(); + const [ + { pageParam, searchParam }, + { handlePageChange, handleSearchTermChange }, + ] = useCustomPagination(); + + const pauseActiveEventAction = useActionWithPath({ + onSuccess: () => { + refetch(); + }, + onError: (error: Response) => { + logger.error(error); + refetch(); + }, + }); + + const [activeFilterParam, setActiveFilterParam] = useQueryState( + ACTIVE_FILTER_QUERY_PARAM, + "all", + ); + + const activeNonActiveFiltered = useMemo( + () => + eventHandlers.filter( + ({ active }: ConductorEvent) => + activeFilterParam === "all" || + (activeFilterParam === "yes" && active) || + (activeFilterParam === "no" && !active), + ), + [eventHandlers, activeFilterParam], + ); + + const handlePauseResumeEvent = useCallback( + (event: ConductorEvent, active: boolean) => { + if (event) { + // @ts-ignore + pauseActiveEventAction.mutate({ + method: "put", + path: `/event`, + body: JSON.stringify({ + ...event, + active, + }), + }); + setToast({ + isOpen: true, + message: `${event.name} is now ${active ? "running" : "paused"}.`, + status: `${active ? "running" : "paused"}`, + }); + } + }, + [pauseActiveEventAction], + ); + + const [confirmDeleteName, setConfirmDeleteName] = useState(""); + + const columns = [ + { + id: "name", + name: "name", + label: "Event handler name", + renderer: (name: string, rec: ConductorEvent) => ( + + {name} + + ), + }, + { id: "event", name: "event", label: "Event" }, + { + id: "event_tags", + name: "tags", + label: "Tags", + searchable: true, + searchableFunc: (tags: TagDto[]) => createSearchableTags(tags), + renderer: TagsRenderer, + grow: 2, + tooltip: "The tags associated with the event handler", + }, + { + id: "active", + name: "active", + label: "Status", + searchable: true, + searchableFunc: getStatusLabel, + renderer(status: boolean) { + return ( + + + + ); + }, + }, + { + id: "actions", + name: "actions", + label: "Actions", + sortable: false, + searchable: false, + grow: 0.5, + right: true, + renderer: (__: string, taskRowData: any) => ( + + {taskRowData.active && ( + + handlePauseResumeEvent(taskRowData, false)} + color="primary" + disabled={isTrialExpired} + size="small" + > + + + + )} + + { + setAddTagDialogData({ + tags: taskRowData.tags || [], + itemName: taskRowData.name, + itemType: "event", + } as TagDialogProps); + setShowAddTagDialog(true); + }} + size="small" + > + + + + {!taskRowData.active && ( + + handlePauseResumeEvent(taskRowData, true)} + color="primary" + size="small" + disabled={isTrialExpired} + > + + + + )} + + { + setConfirmDeleteName(taskRowData?.name); + }} + disabled={isTrialExpired} + size="small" + sx={{ + whiteSpace: "nowrap", + }} + > + + + + + ), + }, + ]; + + const deleteEventHandler = useActionWithPath({ + onSuccess: () => { + refetch(); + }, + onError: (err: Error) => { + logger.error(err); + refetch(); + }, + }); + + const handleClickDefineEventHandler = () => { + pushHistory(EVENT_HANDLERS_URL.NEW); + }; + + return ( + + + Event Handler Definitions + + {confirmDeleteName && ( + { + if (selectedChoice) { + // @ts-ignore + deleteEventHandler.mutate({ + method: "delete", + path: encodeURI(`/event/${confirmDeleteName}`), + }); + } + setConfirmDeleteName(""); + }} + message={ + <> + <> + Are you sure you want to delete{" "} + {confirmDeleteName}{" "} + Event Handler definition? This change cannot be undone. +
    + Please type {confirmDeleteName} to confirm +
    + + + } + header="Delete event handler" + valueToBeDeleted={confirmDeleteName} + isInputConfirmation + /> + )} + {toast.isOpen && ( + setToast({ isOpen: false, message: "", status: "" })} + anchorOrigin={{ + vertical: "bottom", + horizontal: "right", + }} + /> + )} + + { + setShowAddTagDialog(false); + setAddTagDialogData(null); + }} + onSuccess={() => { + setShowAddTagDialog(false); + setAddTagDialogData(null); + refetch(); + }} + apiPath={`/event/${addTagDialogData?.itemName}/tags`} + /> + + pushHistory(EVENT_HANDLERS_URL.NEW), + startIcon: , + }, + ]} + /> + } + /> + + + + + + + + + + + + + + + setActiveFilterParam(e.target.value) + } + sx={{ marginLeft: 2 }} + > + {activeFilterGroups.map((item, index) => ( + } + label={item.title} + /> + ))} + + + } + label="Active?:" + sx={{ + marginLeft: 0, + "& .MuiFormControlLabel-label": { + fontWeight: 100, + color: "gray", + }, + }} + /> + + + + + +
    + null} + searchTerm={searchParam} + onSearchTermChange={handleSearchTermChange} + noDataComponent={ + searchParam === "" ? ( + + ) : ( + handleSearchTermChange("")} + /> + ) + } + defaultShowColumns={["name", "event", "tags", "actions"]} + keyField="name" + data={activeNonActiveFiltered} + columns={columns} + customActions={[ + + + , + ]} + onChangePage={handlePageChange} + paginationDefaultPage={pageParam ? Number(pageParam) : 1} + /> + + + + ); +} diff --git a/ui-next/src/pages/definitions/Scheduler/BulkActionModule.tsx b/ui-next/src/pages/definitions/Scheduler/BulkActionModule.tsx new file mode 100644 index 0000000000..927b4182d5 --- /dev/null +++ b/ui-next/src/pages/definitions/Scheduler/BulkActionModule.tsx @@ -0,0 +1,234 @@ +import React, { SyntheticEvent, useState } from "react"; +import { + Box, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Tab, + Tabs, + Typography, +} from "@mui/material"; +import { useAction } from "utils/query"; +import { + Button, + DataTable, + DropdownButton, + Heading, + LinearProgress, +} from "components"; + +interface TabPanelProps { + children?: React.ReactNode; + index: number; + value: number; +} + +const styles = { + clickSearch: { + width: "100%", + padding: "30px", + paddingBottom: "0px", + display: "block", + textAlign: "center", + }, + paper: { + marginBottom: "30px", + }, + heading: { + marginBottom: "20px", + minHeight: "60px", + }, + controls: { + // padding: 15, + }, + popupIndicator: { + backgroundColor: "red", + }, + banner: { + marginBottom: "15px", + }, + actionBar: { + display: "flex", + alignItems: "center", + paddingRight: "10px", + "&>div, &>p": { + marginRight: "10px", + }, + width: "100%", + justifyContent: "space-between", + }, +}; + +function TabPanel(props: TabPanelProps) { + const { children, value, index, ...other } = props; + + return ( + + ); +} + +export default function BulkActionModule({ + selectedRows, + refetchExecution, + handleError, +}: { + selectedRows: any[]; + refetchExecution: () => void; + handleError: (error: any) => void; +}) { + const selectedIds = selectedRows.map((row) => row.name); + const [results, setResults] = useState(null); + const [tab, setTab] = useState(0); + + const { mutate: pauseAction, isLoading: pauseLoading } = useAction( + `/scheduler/bulk/pause`, + "put", + { onSuccess, onError }, + ); + const { mutate: resumeAction, isLoading: resumeLoading } = useAction( + `/scheduler/bulk/resume`, + "put", + { onSuccess, onError }, + ); + + const isLoading = pauseLoading || resumeLoading; + + function onSuccess(data: any) { + const retval = { + bulkErrorResults: Object.entries(data.bulkErrorResults).map( + ([key, value]) => ({ + name: key, + message: value, + }), + ), + bulkSuccessfulResults: data.bulkSuccessfulResults.map( + (value: string) => ({ + name: value, + }), + ), + }; + setResults(retval); + } + + function onError(error: any) { + handleError(error); + } + + function handleClose() { + setResults(null); + setTab(0); + refetchExecution(); + } + + const handleTabChange = (_event: SyntheticEvent, newValue: number) => { + setTab(newValue); + }; + + return ( + + {selectedRows.length} Schedules Selected. + {/*@ts-ignore*/} + pauseAction({ body: JSON.stringify(selectedIds) }), + }, + { + label: "Resume", + // @ts-ignore + handler: () => resumeAction({ body: JSON.stringify(selectedIds) }), + }, + ]} + > + Bulk Action + + {(results || isLoading) && ( + + + Batch Actions + + + {isLoading && } + {results && ( + + + + + + + + + 15} + /> + + + 15} + /> + + + )} + + + + + + )} + + ); +} diff --git a/ui-next/src/pages/definitions/Scheduler/Schedules.tsx b/ui-next/src/pages/definitions/Scheduler/Schedules.tsx new file mode 100644 index 0000000000..b7cb06f4be --- /dev/null +++ b/ui-next/src/pages/definitions/Scheduler/Schedules.tsx @@ -0,0 +1,892 @@ +import { + Box, + FormControl, + FormControlLabel, + Grid, + Radio, + RadioGroup, + Tooltip, +} from "@mui/material"; +import { + CopySimple as CopyIcon, + Trash as DeleteIcon, + PauseCircle as PauseIcon, + ArrowClockwise as RefreshIcon, + Tag as TagIcon, +} from "@phosphor-icons/react"; +import { Button, DataTable, IconButton, NavLink } from "components"; +import { ColumnCustomType } from "components/DataTable/types"; +import Header from "components/Header"; +import NoDataComponent from "components/NoDataComponent"; +import Paper from "components/Paper"; +import { SnackbarMessage } from "components/SnackbarMessage"; +import TagChip from "components/TagChip"; +import ConfirmChoiceDialog from "components/ConfirmChoiceDialog"; +import AddTagDialog from "components/tags/AddTagDialog"; +import ConductorInput from "components/v1/ConductorInput"; +import TagList from "components/v1/TagList"; +import AddIcon from "components/v1/icons/AddIcon"; +import PlayIcon from "components/v1/icons/PlayIcon"; +import cronstrue from "cronstrue"; +import { useSaveSchedule } from "pages/scheduler/schedulerHooks"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { Helmet } from "react-helmet"; +import { useQueryState } from "react-router-use-location-state"; +import SectionContainer from "shared/SectionContainer"; +import SectionHeader from "shared/SectionHeader"; +import SectionHeaderActions from "shared/SectionHeaderActions"; +import { useAuth } from "shared/auth"; +import { colors } from "theme/tokens/variables"; +import { PopoverMessage } from "types/Messages"; +import { IScheduleDto, IStartWorkflowRequest } from "types/Schedulers"; +import { TagDto } from "types/Tag"; +import { HTTPMethods } from "types/TaskType"; +import { getSequentiallySuffix, logger } from "utils"; +import { + ACTIVE_FILTER_QUERY_PARAM, + generateForbiddenMessage, +} from "utils/constants/common"; +import { SCHEDULER_DEFINITION_URL } from "utils/constants/route"; +import { + useGetSchedulerDefinitions, + useGetSchedulerDefinitionsWithPagination, + SchedulerSearchParams, +} from "utils/hooks/useGetSchedulerDefinitions"; +import { usePushHistory } from "utils/hooks/usePushHistory"; +import { useActionWithPath } from "utils/query"; +import { createSearchableTags } from "utils/utils"; +import CloneScheduleDialog from "../dialog/CloneScheduleDialog"; +import { + activeFilterGroups, + activeLinkColor, + conditionalRowStyles, + getLinkColor, + pausedLinkColor, + pausedrowColor, +} from "../rowColorHelpers"; +import BulkActionModule from "./BulkActionModule"; + +const INTRO_CONTENT = `Schedulers help you automate workflow execution using cron expressions. Set up recurring workflows with precise timing control, perfect for batch processing, periodic data syncs, or any time-based automation needs. + +Read more: +* [Developer Guides: Scheduling Workflows](https://orkes.io/content/developer-guides/scheduling-workflows) +* [Schedule API Reference](https://orkes.io/content/reference-docs/api/schedule) +`; + +const getNameAndVersion = (workflow: IStartWorkflowRequest | undefined) => { + if (!workflow) { + return "Undefined Workflow"; + } + return workflow.version !== undefined + ? `${workflow.name} - Version: ${workflow.version}` + : `${workflow.name} - Latest`; +}; + +const customSortForWorkflowColumn = ( + rowA: IScheduleDto, + rowB: IScheduleDto, +) => { + const nameWithVersionA = getNameAndVersion(rowA.startWorkflowRequest); + const nameWithVersionB = getNameAndVersion(rowB.startWorkflowRequest); + return nameWithVersionA + .toLowerCase() + .localeCompare(nameWithVersionB.toLowerCase()); +}; + +const searchableWorkflow = (workflow: IStartWorkflowRequest) => { + return workflow.version !== undefined + ? `${workflow.name} - Version: ${workflow.version}` + : `${workflow.name} - Latest`; +}; + +const columns = [ + { + id: "cronExpression", + name: "cronExpression", + label: "Cron expression", + renderer: (cron: string) => { + if (!cron) { + return ""; + } + return ( + + {cron ? cronstrue.toString(cron) : ""} + + ); + }, + tooltip: "Cron expression", + sortable: false, + }, + { + id: "name", + name: "name", + label: "Schedule name", + sortable: true, + renderer: (val: string, row: IScheduleDto) => ( + + {val.trim()} + + ), + grow: 1.3, + tooltip: "The name of the schedule", + }, + { + id: "nextRunTime", + name: "nextRunTime", + label: "Next run time", + type: ColumnCustomType.DATE, + sortable: false, + grow: 1, + tooltip: "The next time the schedule will run", + }, + { + id: "tags", + name: "tags", + label: "Tags", + searchable: true, + sortable: false, + searchableFunc: (tags: TagDto[]) => createSearchableTags(tags), + renderer: (tags: TagDto[], row: IScheduleDto) => ( + + ), + grow: 1, + tooltip: "Tags associated with the schedule", + }, + { + id: "startWorkflowRequest", + name: "startWorkflowRequest", + label: "Workflow", + sortable: true, + grow: 1.5, + searchableFunc: (workflow: IStartWorkflowRequest) => + searchableWorkflow(workflow), + renderer: (val: IStartWorkflowRequest) => { + if (val.version !== undefined) { + return `${val.name} - Version: ${val.version}`; + } else { + return `${val.name} - Latest`; + } + }, + sortFunction: customSortForWorkflowColumn, + tooltip: "The workflow associated with the schedule", + }, + { + id: "createTime", + name: "createTime", + label: "Created time", + type: ColumnCustomType.DATE, + sortable: true, + tooltip: "The time the schedule was created", + }, + { + id: "lastRunTimeInEpoch", + name: "lastRunTimeInEpoch", + label: "Last Run time", + type: ColumnCustomType.DATE, + sortable: false, + tooltip: "The last time the schedule ran", + }, + { + id: "createdBy", + name: "createdBy", + label: "Created by", + grow: 1, + sortable: false, + tooltip: "The user who created the schedule", + }, + { + id: "updatedBy", + name: "updatedBy", + label: "Updated by", + grow: 1, + sortable: false, + tooltip: "The user who last updated the schedule", + }, + { + id: "paused", + name: "active", + label: "Status", + grow: 0.5, + minWidth: "120px", + tooltip: "The status of the schedule", + renderer: (val: boolean) => { + return ( + + + + ); + }, + }, + { + id: "workflowExecutionsLink", + name: "name", + selector: (row: IScheduleDto) => row.name, + label: "Workflow executions", + searchable: false, + grow: 1, + sortable: false, + tooltip: "The workflow executions associated with the schedule", + renderer: (name: string, rec: IScheduleDto) => ( + + Workflow query + + ), + }, + { + id: "schedulerExecutionsLink", + name: "name", + selector: (row: IScheduleDto) => row.name, + label: "Scheduler executions", + searchable: false, + sortable: false, + grow: 1, + tooltip: "The scheduler executions associated with the schedule", + renderer: (name: string, rec: IScheduleDto) => ( + + Scheduler query + + ), + }, +]; + +export default function ScheduleDefinitions() { + const [selectedRows, setSelectedRows] = useState([]); + const [toggleCleared, setToggleCleared] = useState(false); + + // Pagination state + const [page, setPage] = useState(1); + const [rowsPerPage, setRowsPerPage] = useState(15); + const [sort, setSort] = useState(undefined); + const [searchTerm, setSearchTerm] = useState(""); + + const { isTrialExpired } = useAuth(); + const [toast, setToast] = useState({ + isOpen: false, + message: "", + status: "", + }); + + const initialState = { + confirmationDialogDeleteOpen: false, + scheduleName: "", + }; + const [deleteScheduleState, setDeleteScheduleState] = useState(initialState); + const [errorMessage, setErrorMessage] = useState(""); + const [selectedSchedule, setSelectedSchedule] = useState( + null, + ); + const [toastMessage, setToastMessage] = useState(null); + + const [activeFilterParam, setActiveFilterParam] = useQueryState( + ACTIVE_FILTER_QUERY_PARAM, + "all", + ); + + // Build search params for pagination + const searchParams: SchedulerSearchParams = useMemo(() => { + const params: SchedulerSearchParams = { + start: (page - 1) * rowsPerPage, + size: rowsPerPage, + }; + + if (sort) { + params.sort = sort; + } + + if (searchTerm) { + params.name = searchTerm; + } + + // Map active filter to paused parameter + if (activeFilterParam === "yes") { + params.paused = false; // Active schedules (not paused) + } else if (activeFilterParam === "no") { + params.paused = true; // Inactive schedules (paused) + } + // If "all", don't set paused parameter + + return params; + }, [page, rowsPerPage, sort, searchTerm, activeFilterParam]); + + const { + data: paginatedData, + isFetching, + refetch, + } = useGetSchedulerDefinitionsWithPagination(searchParams); + + // For backward compatibility with clone dialog (fetch all schedule names) + const { data: allSchedulesData } = useGetSchedulerDefinitions(); + + useEffect(() => { + setSelectedRows([]); + setToggleCleared((t) => !t); + }, [paginatedData]); + + const handleFetchError = async (error: Response, method: HTTPMethods) => { + logger.error("[Schedules.tsx][handleFetchError] Error:", error); + + if (error.status >= 400) { + switch (error.status) { + case 403: + setErrorMessage(generateForbiddenMessage(method)); + break; + default: { + // Check if the response is JSON + const isJSON = error.headers + .get("content-type") + ?.includes("application/json"); + const response = isJSON ? await error.json() : await error.text(); + + setErrorMessage(isJSON ? response?.message : response); + } + } + } + + refetch(); + }; + + const pushHistory = usePushHistory(); + + const { mutate: saveSchedule, isLoading: isSavingSchedule } = useSaveSchedule( + { + onSuccess: () => { + refetch(); + setSelectedSchedule(null); + setToastMessage({ + text: "Schedule cloned successfully", + severity: "success", + }); + }, + + onError: (error: Response) => handleFetchError(error, HTTPMethods.POST), + }, + ); + + const deleteScheduleAction = useActionWithPath({ + onSuccess: () => { + refetch(); + }, + onError: (error: Response) => handleFetchError(error, HTTPMethods.DELETE), + }); + + const [addTagDialogData, setAddTagDialogData] = useState( + null, + ); + const [showAddTagDialog, setShowAddTagDialog] = useState(false); + + // Transform paginated data to add 'active' field + const schedules = useMemo(() => { + if (paginatedData?.results) { + return paginatedData.results.map((schedule) => ({ + ...schedule, + active: !schedule.paused, + })); + } + return []; + }, [paginatedData]); + + const scheduleNames: string[] = useMemo( + () => + allSchedulesData + ? allSchedulesData.map((schedule: IScheduleDto) => schedule.name) + : [], + [allSchedulesData], + ); + + const totalCount = paginatedData?.totalHits ?? 0; + + const pauseScheduleAction = useActionWithPath({ + onSuccess: () => { + refetch(); + }, + onError: (error: Response) => handleFetchError(error, HTTPMethods.GET), + }); + + const handlePauseSchedule = useCallback( + (scheduleName: string) => { + if (scheduleName) { + // @ts-ignore + pauseScheduleAction.mutate({ + method: "get", + path: `/scheduler/schedules/${scheduleName}/pause`, + }); + setToast({ + isOpen: true, + message: `${scheduleName} is now paused.`, + status: "paused", + }); + } + }, + [pauseScheduleAction], + ); + + const handleResumeSchedule = useCallback( + (scheduleName: string) => { + if (scheduleName) { + // @ts-ignore + pauseScheduleAction.mutate({ + method: "get", + path: `/scheduler/schedules/${scheduleName}/resume`, + }); + setToast({ + isOpen: true, + message: `${scheduleName} is now running.`, + status: "running", + }); + } + }, + [pauseScheduleAction], + ); + + const deleteSchedule = (name: string) => { + if (name && name !== "") { + setDeleteScheduleState((prevState) => ({ + ...prevState, + confirmationDialogDeleteOpen: true, + scheduleName: name, + })); + } else { + logger.log( + "No schedule selected for deletion. Unable to recognize name from the definition.", + ); + } + }; + + const handleDeleteScheduleConfirmation = (val: boolean) => { + setDeleteScheduleState(initialState); + if (val) { + // @ts-ignore + deleteScheduleAction.mutate({ + method: "delete", + path: `/scheduler/schedules/${deleteScheduleState.scheduleName}`, + }); + } + }; + + const renderColumns = useMemo( + () => [ + ...columns, + { + id: "actions", + name: "name", + selector: (row: IScheduleDto) => row.name, + label: "Actions", + right: true, + sortable: false, + searchable: false, + grow: 0.5, + minWidth: "160px", + renderer: (name: string, row: IScheduleDto) => ( + + {row.active && ( + + handlePauseSchedule(name)} + color="primary" + disabled={isTrialExpired} + > + + + + )} + + {!row.active && ( + + handleResumeSchedule(name)} + color="primary" + disabled={isTrialExpired} + > + + + + )} + + + setSelectedSchedule(row)} + size="small" + disabled={isTrialExpired} + sx={{ + whiteSpace: "nowrap", + }} + > + + + + + + { + setAddTagDialogData(row); + setShowAddTagDialog(true); + }} + size="small" + > + + + + + + deleteSchedule(name)} + > + + + + + ), + }, + ], + [handlePauseSchedule, handleResumeSchedule, isTrialExpired], + ); + + const handleClickDefineSchedule = () => { + pushHistory(SCHEDULER_DEFINITION_URL.NEW); + }; + + const handleError = (error: any) => { + setErrorMessage(error?.message); + }; + + // Pagination handlers + const handlePageChange = (newPage: number) => { + setPage(newPage); + }; + + const handleRowsPerPageChange = (newRowsPerPage: number) => { + setRowsPerPage(newRowsPerPage); + setPage(1); // Reset to first page when changing rows per page + }; + + const handleSort = (column: any, sortDirection: string) => { + if (column.id) { + // Format: "fieldName:ASC" or "fieldName:DESC" + const sortParam = `${column.id}:${sortDirection.toUpperCase()}`; + setSort(sortParam); + setPage(1); // Reset to first page when sorting + } + }; + + const handleSearchTermChange = (value: string) => { + setSearchTerm(value); + setPage(1); // Reset to first page when searching + }; + + // Reset page when active filter changes + useEffect(() => { + setPage(1); + }, [activeFilterParam]); + + return ( + <> + + Workflow Scheduler Definitions + + {errorMessage && ( + setErrorMessage("")} + /> + )} + {toast.isOpen && ( + setToast({ isOpen: false, message: "", status: "" })} + anchorOrigin={{ + vertical: "bottom", + horizontal: "right", + }} + /> + )} + {deleteScheduleState.confirmationDialogDeleteOpen && ( + + handleDeleteScheduleConfirmation(val) + } + message={ + <> + Are you sure you want to delete{" "} + + {deleteScheduleState.scheduleName} + {" "} + schedule definition? This action cannot be undone. +
    + Please type {deleteScheduleState.scheduleName}{" "} + to confirm. +
    + + } + header={"Deletion confirmation"} + isInputConfirmation + valueToBeDeleted={deleteScheduleState.scheduleName} + /> + )} + + { + setShowAddTagDialog(false); + }} + onSuccess={() => { + setShowAddTagDialog(false); + refetch(); + }} + apiPath={`/scheduler/schedules/${addTagDialogData?.name}/tags`} + /> + + {selectedSchedule && ( + setSelectedSchedule(null)} + onSuccess={({ name }) => { + // @ts-ignore + saveSchedule({ + body: JSON.stringify({ ...selectedSchedule, name }), + }); + }} + scheduleNames={scheduleNames} + isFetching={isSavingSchedule} + /> + )} + pushHistory(SCHEDULER_DEFINITION_URL.NEW), + startIcon: , + }, + ]} + /> + } + /> + + {/*@ts-ignore*/} + + + + + + + + + + + + + + setActiveFilterParam(e.target.value) + } + sx={{ marginLeft: 2 }} + > + {activeFilterGroups.map((item, index) => ( + } + label={item.title} + /> + ))} + + + } + label="Status:" + sx={{ + marginLeft: 0, + "& .MuiFormControlLabel-label": { + fontWeight: 100, + color: "gray", + }, + }} + /> + + + + + +
    + {schedules != null && paginatedData != null && ( + + + + , + ]} + pagination + paginationServer + paginationTotalRows={totalCount} + paginationDefaultPage={page} + paginationPerPage={rowsPerPage} + onChangePage={handlePageChange} + onChangeRowsPerPage={handleRowsPerPageChange} + sortServer + defaultSortFieldId={sort ? undefined : "createTime"} + defaultSortAsc={false} + onSort={handleSort} + selectableRows + contextComponent={ + + } + onSelectedRowsChange={({ selectedRows }) => + setSelectedRows(selectedRows) + } + clearSelectedRows={toggleCleared} + customStyles={{ + header: { + style: { + overflow: "visible", + }, + }, + contextMenu: { + style: { + display: "none", + }, + activeStyle: { + display: "flex", + }, + }, + }} + noDataComponent={ + + } + /> + + )} + + + {toastMessage && ( + { + setToastMessage(null); + }} + /> + )} + + ); +} diff --git a/ui-next/src/pages/definitions/Task.tsx b/ui-next/src/pages/definitions/Task.tsx new file mode 100644 index 0000000000..0cadccb040 --- /dev/null +++ b/ui-next/src/pages/definitions/Task.tsx @@ -0,0 +1,551 @@ +import { Box, Tooltip } from "@mui/material"; +import { + CopySimple as CopyIcon, + Trash as DeleteIcon, + ArrowClockwise as RefreshIcon, + Tag as TagIcon, +} from "@phosphor-icons/react"; +import { Button, DataTable, IconButton, NavLink, Paper } from "components"; +import { ColumnCustomType } from "components/DataTable/types"; +import ConfirmChoiceDialog from "components/ConfirmChoiceDialog"; +import Header from "components/Header"; +import NoDataComponent from "components/NoDataComponent"; +import { SnackbarMessage } from "components/SnackbarMessage"; +import TagChip from "components/TagChip"; +import AddTagDialog, { TagDialogProps } from "components/tags/AddTagDialog"; +import AddIcon from "components/v1/icons/AddIcon"; +import { MessageContext } from "components/v1/layout/MessageContext"; +import { useCallback, useContext, useMemo, useState } from "react"; +import { Helmet } from "react-helmet"; +import { useAuth } from "shared/auth"; +import SectionContainer from "shared/SectionContainer"; +import SectionHeader from "shared/SectionHeader"; +import SectionHeaderActions from "shared/SectionHeaderActions"; +import { colors } from "theme/tokens/variables"; +import { TaskDto } from "types"; +import { PopoverMessage } from "types/Messages"; +import { TagDto } from "types/Tag"; +import { NEW_TASK_DEF_URL, TASK_DEF_URL } from "utils/constants/route"; +import { featureFlags, FEATURES } from "utils/flags"; +import { parseErrorResponse } from "utils/helpers"; +import useCustomPagination from "utils/hooks/useCustomPagination"; +import { usePushHistory } from "utils/hooks/usePushHistory"; +import { logger } from "utils/logger"; +import { useAction, useActionWithPath, useFetch } from "utils/query"; +import { getSequentiallySuffix } from "utils/strings"; +import { createSearchableTags } from "utils/utils"; +import CloneDialog from "./dialog/CloneDialog"; +import TagList from "components/v1/TagList"; + +const INTRO_CONTENT = `A **task definition** defines the task's default parameters, such as input/output schemas, timeouts, and retries. +This provides reusability and modularity across workflows. + +The task definition names match the name of your workers. + +These defaults can be overridden by the **Task Configuration** section in a workflow. + +Read more: + +* [Core Concepts: Tasks](https://orkes.io/content/core-concepts#task-definition) +* [Developer Guides: Tasks](https://orkes.io/content/developer-guides/tasks) +* [Task Definition API Docs](https://orkes.io/content/reference-docs/api/metadata/creating-task-definitions) +`; + +export default function TaskDefinitions() { + const [confirmDeleteName, setConfirmDeleteName] = useState(""); + const [showAddTagDialog, setShowAddTagDialog] = useState(false); + const [addTagDialogData, setAddTagDialogData] = + useState(null); + const [{ pageParam, searchParam }, { setSearchParam, handlePageChange }] = + useCustomPagination(); + + const [selectedTask, setSelectedTask] = useState(null); + const [toastMessage, setToastMessage] = useState(null); + + const { setMessage } = useContext(MessageContext); + const { isTrialExpired } = useAuth(); + + const columns = useMemo( + () => [ + { + id: "name", + name: "name", + label: "Task name", + renderer: (name: string) => ( + + {name} + + ), + tooltip: "Task name", + }, + { + id: "executable", + name: "executable", + label: "Executable?", + renderer: (executable: boolean) => ( + + ), + tooltip: + "Tasks marked as Yes are available for you to execute. If you need access to execute any other task, please contact the task owner or your Administrator.", + }, + { + id: "description", + name: "description", + label: "Description", + grow: 2, + tooltip: "Task description", + }, + { + id: "createTime", + name: "createTime", + label: "Created time", + type: ColumnCustomType.DATE, + tooltip: "Task created time", + }, + { + id: "ownerEmail", + name: "ownerEmail", + label: "Owner email", + tooltip: "Task owner email", + }, + { + id: "inputKeys", + name: "inputKeys", + label: "Input keys", + type: ColumnCustomType.JSON, + sortable: false, + tooltip: "Task input keys", + }, + { + id: "outputKeys", + name: "outputKeys", + label: "Output keys", + type: ColumnCustomType.JSON, + sortable: false, + tooltip: "Task output keys", + }, + { + id: "timeoutPolicy", + name: "timeoutPolicy", + label: "Timeout policy", + grow: 0.5, + tooltip: "Task timeout policy", + }, + { + id: "timeoutSeconds", + name: "timeoutSeconds", + label: "Timeout seconds", + grow: 0.5, + tooltip: "Task timeout seconds", + }, + { + id: "retryCount", + name: "retryCount", + label: "Retry count", + grow: 0.5, + tooltip: "Task retry count", + }, + { + id: "retryLogic", + name: "retryLogic", + label: "Retry logic", + tooltip: "Task retry logic", + }, + { + id: "retryDelaySeconds", + name: "retryDelaySeconds", + label: "Retry delay seconds", + grow: 0.5, + tooltip: "Task retry delay seconds", + }, + { + id: "responseTimeoutSeconds", + name: "responseTimeoutSeconds", + label: "Response timeout seconds", + grow: 0.5, + tooltip: "Task response timeout seconds", + }, + { + id: "inputTemplate", + name: "inputTemplate", + label: "Input template", + type: ColumnCustomType.JSON, + sortable: false, + tooltip: "Task input template", + }, + { + id: "rateLimitPerFrequency", + name: "rateLimitPerFrequency", + label: "Rate limit per freq", + grow: 0.5, + tooltip: "Task rate limit per frequency", + }, + { + id: "rateLimitFrequencyInSeconds", + name: "rateLimitFrequencyInSeconds", + label: "Rate limit freq in seconds", + grow: 0.5, + tooltip: "Task rate limit frequency in seconds", + }, + { + id: "tags", + name: "tags", + label: "Tags", + searchable: true, + tooltip: "Task tags", + searchableFunc: (tags: TagDto[]) => createSearchableTags(tags), + renderer: (tags: TagDto[], row: TaskDto) => ( + + ), + grow: 2, + }, + { + id: "actions", + name: "name", + label: "Actions", + sortable: false, + searchable: false, + grow: 0.5, + minWidth: "130px", + tooltip: "Actions that can be performed on the task definition", + renderer: (name: string, taskRowData: TaskDto) => ( + + + setSelectedTask(taskRowData)} + disabled={isTrialExpired} + size="small" + sx={{ + whiteSpace: "nowrap", + }} + > + + + + + + { + setAddTagDialogData({ + apiPath: "", + onClose(): void {}, + onSuccess(): void {}, + tags: taskRowData.tags || [], + itemName: taskRowData.name, + itemType: "task", + } as TagDialogProps); + setShowAddTagDialog(true); + }} + size="small" + > + + + + + + { + setConfirmDeleteName(name); + }} + size="small" + color="error" + sx={{ + whiteSpace: "nowrap", + }} + > + + + + + ), + }, + ], + [isTrialExpired], + ); + + const taskVisibility = featureFlags.getValue( + FEATURES.TASK_VISIBILITY, + "READ", + ); + const pushHistory = usePushHistory(); + const { + data: visibilityData, + isFetching, + refetch, + } = useFetch(`/metadata/taskdefs?access=${taskVisibility}&metadata=true`); + + const { + data: readonlyData, + isFetching: isReadonlyDataFetching, + refetch: refetchReadonlyData, + } = useFetch(`/metadata/taskdefs?access=READ&metadata=true`); + + const refetchData = () => { + refetch(); + refetchReadonlyData(); + }; + + const deleteTaskDefinitionAction = useActionWithPath({ + onSuccess: () => { + refetchData(); + }, + onError: async (err: any) => { + const message = await err?.json(); + setMessage({ + text: message?.message, + severity: "error", + }); + logger.error(err); + refetchData(); + }, + }); + + const tableData = useMemo( + () => + readonlyData && visibilityData + ? readonlyData.reduce((result: TaskDto[], currentItem: TaskDto) => { + const executable = + visibilityData.findIndex( + (item: TaskDto) => item.name === currentItem.name, + ) > -1; + result.push({ + createTime: !currentItem.createTime ? 0 : currentItem.createTime, + ...currentItem, + executable, + }); + + return result; + }, []) + : [], + [visibilityData, readonlyData], + ); + + const handleSearchTermChange = useCallback( + (searchTerm: string) => { + setSearchParam(searchTerm); + }, + [setSearchParam], + ); + + const handleClickDefineTask = () => { + pushHistory(NEW_TASK_DEF_URL); + }; + + const taskNameList: string[] = useMemo( + () => (tableData ? tableData.map((task: TaskDto) => task.name) : []), + [tableData], + ); + + const { mutate: saveTask, isLoading: isSavingTask } = useAction( + "/metadata/taskdefs", + "post", + { + onSuccess: () => { + refetchData(); + setSelectedTask(null); + setToastMessage({ + text: "Task cloned successfully", + severity: "success", + }); + }, + onError: async (error: Response) => { + logger.error(error); + const errorMessage = await parseErrorResponse({ + response: error, + module: "task", + operation: "cloning", + }); + setToastMessage({ + text: errorMessage, + severity: "error", + }); + }, + }, + ); + + return ( + <> + + Task Definitions + + + {selectedTask && ( + setSelectedTask(null)} + onSuccess={({ name }) => { + const newTaskDefinition = { ...selectedTask, name }; + // @ts-ignore + saveTask({ + body: JSON.stringify([newTaskDefinition]), + }); + }} + isFetching={isSavingTask} + title="Clone Task Confirmation" + id="task-name-field" + label="Task name" + /> + )} + + { + setShowAddTagDialog(false); + setAddTagDialogData(null); + }} + onSuccess={() => { + setShowAddTagDialog(false); + refetchData(); + }} + /> + + {confirmDeleteName && ( + { + if (selectedChoice && confirmDeleteName) { + // @ts-ignore + deleteTaskDefinitionAction.mutate({ + method: "delete", + path: `/metadata/taskdefs/${encodeURIComponent( + confirmDeleteName, + )}`, + }); + } + setConfirmDeleteName(""); + }} + message={ + <> + Are you sure you want to delete{" "} + {confirmDeleteName} task + definition? This cannot be undone. +
    + Please type {confirmDeleteName} to confirm. +
    + + } + header={"Deletion Confirmation"} + isInputConfirmation + valueToBeDeleted={confirmDeleteName} + /> + )} + pushHistory(NEW_TASK_DEF_URL), + startIcon: , + }, + ]} + /> + } + /> + + {/*@ts-ignore*/} + +
    + {tableData && ( + <> + + + , + ]} + onChangePage={handlePageChange} + paginationDefaultPage={pageParam ? Number(pageParam) : 1} + noDataComponent={ + searchParam === "" ? ( + + ) : ( + handleSearchTermChange("")} + /> + ) + } + /> + + )} + + + {toastMessage && ( + { + setToastMessage(null); + }} + /> + )} + + ); +} diff --git a/ui-next/src/pages/definitions/Workflow.tsx b/ui-next/src/pages/definitions/Workflow.tsx new file mode 100644 index 0000000000..675ba879bc --- /dev/null +++ b/ui-next/src/pages/definitions/Workflow.tsx @@ -0,0 +1,560 @@ +import { Box, Tooltip } from "@mui/material"; +import { + CopySimple as CopyIcon, + Trash as DeleteIcon, + ArrowClockwise as RefreshIcon, + Tag as TagIcon, +} from "@phosphor-icons/react"; +import { Button, DataTable, IconButton, NavLink, Paper } from "components"; +import { FilterObjectItem } from "components/DataTable/state"; +import { ColumnCustomType, LegacyColumn } from "components/DataTable/types"; +import Header from "components/Header"; +import NoDataComponent from "components/NoDataComponent"; +import { SnackbarMessage } from "components/SnackbarMessage"; +import ConfirmChoiceDialog from "components/ConfirmChoiceDialog"; +import AddTagDialog, { TagDialogProps } from "components/tags/AddTagDialog"; +import TagList from "components/v1/TagList"; +import PlayIcon from "components/v1/icons/PlayIcon"; +import { MessageContext } from "components/v1/layout/MessageContext"; +import SplitWorkflowDefinitionButton from "pages/executions/SplitWorkflowDefinitionButton/SplitWorkflowDefinitionButton"; +import { removeDeletedWorkflow } from "pages/runWorkflow/runWorkflowUtils"; +import { useCallback, useContext, useMemo, useState } from "react"; +import { Helmet } from "react-helmet"; +import { UseQueryResult } from "react-query"; +import { useNavigate } from "react-router"; +import SectionContainer from "shared/SectionContainer"; +import SectionHeader from "shared/SectionHeader"; +import SectionHeaderActions from "shared/SectionHeaderActions"; +import { useAuth } from "shared/auth"; +import { colors } from "theme/tokens/variables"; +import { PopoverMessage } from "types/Messages"; +import { TagDto } from "types/Tag"; +import { WorkflowDef } from "types/WorkflowDef"; +import { + RUN_WORKFLOW_URL, + WORKFLOW_DEFINITION_URL, +} from "utils/constants/route"; +import { featureFlags, FEATURES } from "utils/flags"; +import useCustomPagination from "utils/hooks/useCustomPagination"; +import { usePushHistory } from "utils/hooks/usePushHistory"; +import { logger } from "utils/logger"; +import { useActionWithPath, useWorkflowDefs } from "utils/query"; +import { createSearchableTags, tryToJson } from "utils/utils"; +import { getUniqueWorkflows } from "utils/workflow"; +import CloneWorkflowDialog from "./dialog/CloneWorkflowDialog"; + +const INTRO_CONTENT = `A **workflow definition** is a blueprint that defines the sequence of tasks, their dependencies, and how data flows between them. + +Workflows can be versioned, tagged, and reused across your applications. They provide a visual and programmatic way to orchestrate complex business processes. + +Read more: + +* [Core Concepts: Workflows](https://orkes.io/content/core-concepts#workflow-definition) +* [Developer Guides: Workflows](https://orkes.io/content/developer-guides/workflows) +* [Workflow API Reference](https://orkes.io/content/reference-docs/api/workflow) + +Browse our templates to get started with easy examples! +`; + +export default function WorkflowDefinitions() { + const navigate = useNavigate(); + const { isTrialExpired } = useAuth(); + + const isPlayground = featureFlags.isEnabled(FEATURES.PLAYGROUND); + const { data, isFetching, refetch }: UseQueryResult = + useWorkflowDefs(); + const [showAddTagDialog, setShowAddTagDialog] = useState(false); + const [addTagDialogData, setAddTagDialogData] = + useState(null); + + const [selectedWorkflowWithAction, setSelectedWorkflowWithAction] = useState<{ + selectedWorkflow: WorkflowDef | null; + action: string; + }>({ + selectedWorkflow: null, + action: "", + }); + const [toastMessage, setToastMessage] = useState(null); + + const { setMessage } = useContext(MessageContext); + const pushHistory = usePushHistory(); + const [ + { filterParam, pageParam, searchParam }, + { setFilterParam, setSearchParam, handlePageChange }, + ] = useCustomPagination(); + const [confirmDelete, setConfirmDelete] = useState<{ + confirmDelete: boolean; + workflowName: string; + workflowVersion: number; + } | null>(null); + const filterObj = + filterParam === "" ? undefined : tryToJson(filterParam); + + const deleteWorkflowVersionAction = useActionWithPath({ + onSuccess: () => { + if (confirmDelete?.workflowName) { + removeDeletedWorkflow( + encodeURIComponent(confirmDelete?.workflowName), + confirmDelete?.workflowVersion, + ); + } + + refetch(); + }, + onError: (err: Error) => { + setMessage({ + severity: "error", + text: "Failed to delete workflow", + }); + logger.error(err); + refetch(); + }, + }); + + const columns = useMemo( + () => [ + { + id: "workflow_name", + name: "name", + label: "Workflow name", + renderer: (val: string) => { + return ( + + {val.trim()} + + ); + }, + tooltip: "The name of the workflow", + }, + { + id: "workflow_description", + name: "description", + label: "Description", + grow: 2, + tooltip: "The description of the workflow", + }, + { + id: "workflow_tags", + name: "tags", + label: "Tags", + searchable: true, + searchableFunc: (tags: TagDto[]) => createSearchableTags(tags), + renderer: (tags: TagDto[], row: WorkflowDef) => ( + + ), + grow: 2, + tooltip: "The tags associated with the workflow", + }, + { + id: "create_time", + name: "createTime", + label: "Created time", + type: ColumnCustomType.DATE, + tooltip: "The time the workflow was created", + }, + { + id: "latest_version", + name: "version", + label: "Latest version", + grow: 0.5, + tooltip: "The latest version of the workflow", + }, + { + id: "schema_version", + name: "schemaVersion", + label: "Schema version", + grow: 0.5, + tooltip: "The schema version of the workflow", + }, + { + id: "restartable", + name: "restartable", + label: "Restartable", + grow: 0.5, + tooltip: "Whether the workflow is restartable", + }, + { + id: "status_listener_enabled", + name: "workflowStatusListenerEnabled", + label: "Status listener enabled", + grow: 0.5, + tooltip: "Whether the status listener is enabled", + }, + { + id: "owner_email", + name: "ownerEmail", + label: "Owner email", + tooltip: "The email of the owner of the workflow", + }, + { + id: "input_params", + name: "inputParameters", + label: "Input params", + type: ColumnCustomType.JSON, + sortable: false, + tooltip: "The input parameters of the workflow", + }, + { + id: "output_params", + name: "outputParameters", + label: "Output params", + type: ColumnCustomType.JSON, + sortable: false, + tooltip: "The output parameters of the workflow", + }, + { + id: "timeout_policy", + name: "timeoutPolicy", + label: "Timeout policy", + grow: 0.5, + tooltip: "The timeout policy of the workflow", + }, + { + id: "timeout_seconds", + name: "timeoutSeconds", + label: "Timeout seconds", + grow: 0.5, + tooltip: "The timeout seconds of the workflow", + }, + { + id: "failure_workflow", + name: "failureWorkflow", + label: "Failure workflow", + grow: 1, + tooltip: "The compensation workflow", + }, + { + id: "executions_link", + name: "name", + label: "Executions", + sortable: false, + searchable: false, + grow: 0.5, + renderer: (name: string) => ( + + Query + + ), + tooltip: "The executions of the workflow", + }, + { + id: "actions", + name: "name", + label: "Actions", + sortable: false, + searchable: false, + grow: 0.5, + minWidth: "180px", + tooltip: "Actions you can perform on the workflow", + renderer: (name: string, workflowRowData: WorkflowDef) => { + return ( + + + { + navigate("/runWorkflow", { + state: { + execution: { + workflowName: workflowRowData.name, + workflowVersion: workflowRowData.version, + input: workflowRowData?.inputParameters + ? Object.fromEntries( + workflowRowData.inputParameters.map((key) => [ + key, + "", + ]), + ) + : {}, + }, + }, + }); + }} + size="small" + sx={{ + whiteSpace: "nowrap", + }} + > + + + + + + + setSelectedWorkflowWithAction({ + selectedWorkflow: workflowRowData, + action: "clone", + }) + } + disabled={isTrialExpired} + size="small" + sx={{ + whiteSpace: "nowrap", + }} + > + + + + + { + setAddTagDialogData({ + tags: workflowRowData.tags || [], + itemName: workflowRowData.name, + itemType: "workflow", + } as TagDialogProps); + setShowAddTagDialog(true); + }} + size="small" + > + + + + + + { + const selectedData = data?.find((x) => x.name === name); + if (selectedData) { + setConfirmDelete({ + confirmDelete: true, + workflowName: selectedData.name, + workflowVersion: selectedData.version, + }); + } + }} + size="small" + sx={{ + whiteSpace: "nowrap", + }} + > + + + + + ); + }, + }, + ], + [data, navigate, isTrialExpired], + ); + + const handleFilterChange = useCallback( + (obj?: FilterObjectItem) => { + if (obj) { + setFilterParam(JSON.stringify(obj)); + } else { + setFilterParam(""); + } + }, + [setFilterParam], + ); + + const workflows = useMemo(() => { + // Extract latest versions only + if (data) { + return getUniqueWorkflows(data); + } + }, [data]); + + const handleClickBrowseTemplates = () => { + pushHistory(isPlayground ? "/" : WORKFLOW_DEFINITION_URL.NEW); + }; + + return ( + <> + + Workflow Definitions + + + {selectedWorkflowWithAction && + selectedWorkflowWithAction?.selectedWorkflow && + selectedWorkflowWithAction?.action === "clone" && ( + + setSelectedWorkflowWithAction({ + selectedWorkflow: null, + action: "", + }) + } + onSuccess={() => { + setSelectedWorkflowWithAction({ + selectedWorkflow: null, + action: "", + }); + refetch(); + setToastMessage({ + text: "Workflow cloned successfully", + severity: "success", + }); + }} + selectedWorkflow={selectedWorkflowWithAction?.selectedWorkflow} + workflowList={data ?? []} + /> + )} + + { + setShowAddTagDialog(false); + setAddTagDialogData(null); + }} + onSuccess={() => { + setShowAddTagDialog(false); + setAddTagDialogData(null); + refetch(); + }} + /> + + {confirmDelete && ( + { + if (selectedChoice) { + // @ts-ignore + deleteWorkflowVersionAction.mutate({ + method: "delete", + path: `/metadata/workflow/${encodeURIComponent( + confirmDelete.workflowName, + )}/${confirmDelete.workflowVersion}`, + }); + } + setConfirmDelete(null); + }} + message={ + <> + Are you sure you want to delete{" "} + + {confirmDelete.workflowName} + {" "} + workflow definition? This cannot be undone. +
    + Please type {confirmDelete.workflowName} to + confirm. +
    + + } + header={"Deletion confirmation"} + isInputConfirmation + valueToBeDeleted={confirmDelete.workflowName} + /> + )} + pushHistory(RUN_WORKFLOW_URL), + startIcon: , + }, + { + customButtonElement: , + }, + ]} + /> + } + /> + + +
    + {workflows && ( + + + , + ]} + onChangePage={handlePageChange} + paginationDefaultPage={pageParam ? Number(pageParam) : 1} + noDataComponent={ + searchParam === "" ? ( + + ) : ( + setSearchParam("")} + /> + ) + } + /> + )} + + + {toastMessage && ( + { + setToastMessage(null); + }} + /> + )} + + ); +} diff --git a/ui-next/src/pages/definitions/dialog/CloneDialog.tsx b/ui-next/src/pages/definitions/dialog/CloneDialog.tsx new file mode 100644 index 0000000000..86f0187b90 --- /dev/null +++ b/ui-next/src/pages/definitions/dialog/CloneDialog.tsx @@ -0,0 +1,115 @@ +import { yupResolver } from "@hookform/resolvers/yup"; +import { + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Grid, +} from "@mui/material"; +import ActionButton from "components/ActionButton"; +import Button from "components/MuiButton"; +import ReactHookFormInput from "components/v1/react-hook-form/ReactHookFormInput"; +import { DefaultValues, SubmitHandler, useForm } from "react-hook-form"; +import { WORKFLOW_NAME_ERROR_MESSAGE } from "utils/constants/common"; +import { WORKFLOW_NAME_REGEX } from "utils/constants/regex"; +import * as yup from "yup"; + +interface DialogData { + name: string; +} +export interface CloneDialogProps { + name: string; + namesList: string[]; + onClose: () => void; + onSuccess: (data: DialogData) => void; + isFetching?: boolean; + title?: string; + id?: string; + label?: string; +} + +const CloneDialog = ({ + name, + onClose, + onSuccess, + namesList, + isFetching, + title, + id, + label, +}: CloneDialogProps) => { + const formSchema = yup.object().shape({ + name: yup + .string() + .required("Name cannot be blank.") + .matches(WORKFLOW_NAME_REGEX, WORKFLOW_NAME_ERROR_MESSAGE) + .notOneOf(namesList, "This name is existing."), + }); + + const defaultValues: DefaultValues = { + name: name, + }; + + const { + control, + handleSubmit, + formState: { errors: formErrors, isValid }, + } = useForm({ + mode: "onChange", + resolver: yupResolver(formSchema), + defaultValues, + }); + + const onSubmit: SubmitHandler = (data) => { + onSuccess(data); + }; + + return ( + + {title} + + + + + + + + + + handleSubmit(onSubmit)()} + disabled={!isValid} + progress={isFetching} + > + Clone + + + + ); +}; + +export default CloneDialog; diff --git a/ui-next/src/pages/definitions/dialog/CloneScheduleDialog.tsx b/ui-next/src/pages/definitions/dialog/CloneScheduleDialog.tsx new file mode 100644 index 0000000000..89afdd436d --- /dev/null +++ b/ui-next/src/pages/definitions/dialog/CloneScheduleDialog.tsx @@ -0,0 +1,110 @@ +import { yupResolver } from "@hookform/resolvers/yup"; +import { + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Grid, +} from "@mui/material"; +import { DefaultValues, SubmitHandler, useForm } from "react-hook-form"; +import * as yup from "yup"; + +import ActionButton from "components/ActionButton"; +import Button from "components/MuiButton"; +import ReactHookFormInput from "components/v1/react-hook-form/ReactHookFormInput"; +import { WORKFLOW_NAME_ERROR_MESSAGE } from "utils/constants/common"; +import { WORKFLOW_NAME_REGEX } from "utils/constants/regex"; + +interface DialogData { + name: string; +} +export interface CloneScheduleDialogProps { + name: string; + scheduleNames: string[]; + onClose: () => void; + onSuccess: (data: DialogData) => void; + isFetching?: boolean; +} + +const CloneScheduleDialog = ({ + name, + scheduleNames, + onClose, + onSuccess, + isFetching, +}: CloneScheduleDialogProps) => { + const formSchema = yup.object().shape({ + name: yup + .string() + .required("Name cannot be blank.") + .matches(WORKFLOW_NAME_REGEX, WORKFLOW_NAME_ERROR_MESSAGE) + .notOneOf(scheduleNames, "This name is existing."), + }); + + const defaultValues: DefaultValues = { + name: name, + }; + + const { + control, + handleSubmit, + formState: { errors: formErrors, isValid }, + } = useForm({ + mode: "onChange", + resolver: yupResolver(formSchema), + defaultValues, + }); + + const onSubmit: SubmitHandler = (data) => { + onSuccess(data); + }; + + return ( + + Clone Schedule Confirmation + + + + + + + + + + handleSubmit(onSubmit)()} + disabled={!isValid} + progress={isFetching} + > + Clone + + + + ); +}; + +export default CloneScheduleDialog; diff --git a/ui-next/src/pages/definitions/dialog/CloneWorkflowDialog.tsx b/ui-next/src/pages/definitions/dialog/CloneWorkflowDialog.tsx new file mode 100644 index 0000000000..53bb0cc722 --- /dev/null +++ b/ui-next/src/pages/definitions/dialog/CloneWorkflowDialog.tsx @@ -0,0 +1,220 @@ +import { yupResolver } from "@hookform/resolvers/yup"; +import { + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Grid, +} from "@mui/material"; +import ActionButton from "components/ActionButton"; +import Button from "components/MuiButton"; +import { MessageContext } from "components/v1/layout/MessageContext"; +import ReactHookFormDropdown from "components/v1/react-hook-form/ReactHookFormDropdown"; +import ReactHookFormInput from "components/v1/react-hook-form/ReactHookFormInput"; +import _last from "lodash/last"; +import { getWorkflowDefinitionByNameAndVersion } from "pages/definition/commonService"; +import { useContext, useMemo } from "react"; +import { DefaultValues, SubmitHandler, useForm } from "react-hook-form"; +import { useQueryClient } from "react-query"; +import { HTTPMethods } from "types/TaskType"; +import { WorkflowDef } from "types/WorkflowDef"; +import { WORKFLOW_METADATA_BASE_URL } from "utils/constants/api"; +import { WORKFLOW_NAME_ERROR_MESSAGE } from "utils/constants/common"; +import { WORKFLOW_NAME_REGEX } from "utils/constants/regex"; +import { logger } from "utils/logger"; +import { + useActionWithPath, + useAuthHeaders, + useSharedQueryContext, +} from "utils/query"; +import { getSequentiallySuffix } from "utils/strings"; +import { getUniqueWorkflowsWithVersions } from "utils/workflow"; +import * as yup from "yup"; + +interface DialogData { + name: string; + version: number; +} + +export interface CloneWorkflowDialogProps { + selectedWorkflow: WorkflowDef; + workflowList: WorkflowDef[]; + onClose: () => void; + onSuccess: () => void; +} + +const CloneWorkflowDialog = ({ + selectedWorkflow, + onClose, + onSuccess, + workflowList, +}: CloneWorkflowDialogProps) => { + const authHeaders = useAuthHeaders(); + const queryClient = useQueryClient(); + const { cacheQueryKey } = useSharedQueryContext(); + + const { setMessage } = useContext(MessageContext); + + const createWorkflowAction = useActionWithPath({ + onSuccess: () => { + onSuccess(); + // Clear cache to force re-fetch without waiting stale time + queryClient.removeQueries(cacheQueryKey); + }, + onError: (err: Error) => { + logger.error(err); + }, + }); + + const workflowsWithVersions = useMemo>( + () => getUniqueWorkflowsWithVersions(workflowList), + [workflowList], + ); + + const workflowNames = useMemo( + () => [...workflowsWithVersions.keys()], + [workflowsWithVersions], + ); + + const workflowVersions = useMemo( + () => + workflowsWithVersions.get(selectedWorkflow.name)?.map((item) => item) || + [], + [workflowsWithVersions, selectedWorkflow], + ); + + const { name: suffixedWfName } = getSequentiallySuffix({ + name: selectedWorkflow.name, + refNames: workflowNames, + }); + + const formSchema: yup.ObjectSchema = yup.object().shape({ + name: yup + .string() + .required("Name cannot be blank.") + .matches(WORKFLOW_NAME_REGEX, WORKFLOW_NAME_ERROR_MESSAGE) + .notOneOf(workflowNames, "This name is existing."), + version: yup + .number() + .required("Version cannot be blank.") + .typeError("Version cannot be blank."), + }); + + const defaultValues: DefaultValues = { + name: suffixedWfName, + version: _last(workflowVersions), + }; + + const { + control, + handleSubmit, + formState: { errors: formErrors, isValid }, + } = useForm({ + mode: "onChange", + resolver: yupResolver(formSchema), + defaultValues, + }); + + const onSubmit: SubmitHandler = async (workflowData) => { + const { name: newName, version } = workflowData; + + // Checking existing cloned workflow + const existingWorkflow: WorkflowDef | undefined = workflowList?.find( + (workflow) => workflow.name === newName, + ); + + if (!existingWorkflow) { + try { + const clonedWorkflow = await getWorkflowDefinitionByNameAndVersion({ + name: selectedWorkflow.name, + version: Number(version), + authHeaders, + }); + + if (clonedWorkflow?.name) { + // @ts-ignore + createWorkflowAction.mutate({ + method: HTTPMethods.POST, + path: WORKFLOW_METADATA_BASE_URL, + body: JSON.stringify({ + ...clonedWorkflow, + version: 1, + name: newName, + }), + workflowName: newName, + }); + } + } catch (e: any) { + setMessage({ + severity: "error", + text: `Unable to clone: ${e.text}`, + }); + onClose(); + } + } + }; + + return ( + + Clone Workflow Confirmation + + + + + + + + option?.toString()} + options={workflowVersions} + error={!!formErrors?.version?.message} + helperText={formErrors?.version?.message} + /> + + + + + + handleSubmit(onSubmit)()} + disabled={!isValid} + progress={createWorkflowAction.isLoading} + > + Clone + + + + ); +}; + +export default CloneWorkflowDialog; diff --git a/ui-next/src/pages/definitions/dialog/ShareWorkflowDialog.tsx b/ui-next/src/pages/definitions/dialog/ShareWorkflowDialog.tsx new file mode 100644 index 0000000000..9f2550f4dd --- /dev/null +++ b/ui-next/src/pages/definitions/dialog/ShareWorkflowDialog.tsx @@ -0,0 +1,258 @@ +import { + Avatar, + Box, + FormControlLabel, + Grid, + IconButton, + Tooltip, +} from "@mui/material"; +import { Share, X } from "@phosphor-icons/react"; +import { Button, Paper } from "components"; +import MuiCheckbox from "components/MuiCheckbox"; +import UIModal from "components/UIModal"; +import ConductorInput from "components/v1/ConductorInput"; +import { MessageContext } from "components/v1/layout/MessageContext"; +import _ from "lodash"; +import _last from "lodash/last"; +import { ChangeEvent, useContext, useEffect, useState } from "react"; +import { useQueryClient } from "react-query"; +import { HTTPMethods } from "types/TaskType"; +import { WorkflowDef } from "types/WorkflowDef"; +import { logger } from "utils/logger"; +import { useActionWithPath, useSharedQueryContext } from "utils/query"; + +interface ShareWorkflowDialogProps { + onClose: () => void; + onSuccess: () => void; + selectedWorkflow: WorkflowDef; +} + +const ShareWorkflowDialog = ({ + onClose, + onSuccess, + selectedWorkflow, +}: ShareWorkflowDialogProps) => { + const queryClient = useQueryClient(); + const { setMessage } = useContext(MessageContext); + const { cacheQueryKey } = useSharedQueryContext(); + const [userId, setUserId] = useState(null); + const [peopleWithAccess, setPeopleWithAccess] = useState([]); + const [shareWithEveryone, setShareWithEveryone] = useState(false); + + const shareWorkflowAction = useActionWithPath({ + onSuccess: () => { + onSuccess(); + setMessage({ + severity: "success", + text: `Workflow shared successfully`, + }); + queryClient.removeQueries(cacheQueryKey); + }, + onError: (err: Error) => { + logger.error(err); + }, + }); + + const getAllsharedResources = useActionWithPath({ + onSuccess: (data: any) => { + const allResources = data; + if (allResources && allResources.length > 0) { + const sharedWithList: string[] = _.chain(allResources) + .filter( + (obj) => + _last(obj.resourceName.split("#")) === selectedWorkflow?.name, + ) + .map("sharedWith") + .value(); + setPeopleWithAccess(sharedWithList); + } + }, + onError: (err: Error) => { + logger.error(err); + }, + }); + + const removeSharingAction = useActionWithPath({ + onSuccess: () => { + setMessage({ + severity: "success", + text: `Access updated`, + }); + setPeopleWithAccess([]); + fetchAllSharedResources(); + }, + onError: (err: Error) => { + logger.error(err); + }, + }); + + const handleUserId = (value: string) => { + setUserId(value); + }; + + const handleShareWithEveryoneChange = ( + event: ChangeEvent, + ) => { + setShareWithEveryone(event.target.checked); + if (event.target.checked) { + setUserId("*"); + } else { + setUserId(null); + } + }; + + const handleShareWorkflow = () => { + try { + // @ts-ignore + shareWorkflowAction.mutate({ + method: HTTPMethods.POST, + path: `/share/shareResource?resourceType=WORKFLOW_DEF&resourceName=${selectedWorkflow?.name}&sharedWith=${userId}`, + }); + } catch (e: any) { + setMessage({ + severity: "error", + text: `Unable to share: ${e.text}`, + }); + } + }; + + const fetchAllSharedResources = () => { + try { + // @ts-ignore + getAllsharedResources.mutate({ + method: HTTPMethods.GET, + path: `/share/getSharedResources`, + }); + } catch (e: any) { + setMessage({ + severity: "error", + text: `Unable to fetch shared resources: ${e.text}`, + }); + } + }; + + useEffect(() => { + fetchAllSharedResources(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const handleRemoveSharing = (user: string) => { + try { + // @ts-ignore + removeSharingAction.mutate({ + method: "delete", + path: `/share/removeSharingResource?resourceType=WORKFLOW_DEF&resourceName=${selectedWorkflow?.name}&sharedWith=${user}`, + }); + } catch (e: any) { + setMessage({ + severity: "error", + text: `Unable to fetch shared resources: ${e.text}`, + }); + } + }; + return ( + } + title={`Share "${selectedWorkflow?.name}" workflow`} + description="Share this workflow with others you choose." + titleSx={{ textTransform: "none" }} + enableCloseButton + > + + + + handleUserId(value)} + autoFocus={true} + disabled={shareWithEveryone} + /> + + } + label="Share this workflow with everyone" + /> + + {peopleWithAccess && peopleWithAccess?.length > 0 ? ( + + + + People with access + + + {peopleWithAccess.map((user, index) => ( + + + {user[0]} + + {user} + + + handleRemoveSharing(user)} + size="small" + sx={{ + whiteSpace: "nowrap", + }} + > + + + + + + ))} + + + + ) : null} + + + + + + + ); +}; + +export default ShareWorkflowDialog; diff --git a/ui-next/src/pages/definitions/index.ts b/ui-next/src/pages/definitions/index.ts new file mode 100644 index 0000000000..e3a41b8b88 --- /dev/null +++ b/ui-next/src/pages/definitions/index.ts @@ -0,0 +1,6 @@ +import EventHandler from "./EventHandler"; +import Schedules from "./Scheduler/Schedules"; +import Task from "./Task"; +import Workflow from "./Workflow"; + +export { EventHandler, Schedules, Task, Workflow }; diff --git a/ui-next/src/pages/definitions/rowColorHelpers.ts b/ui-next/src/pages/definitions/rowColorHelpers.ts new file mode 100644 index 0000000000..572cd6174c --- /dev/null +++ b/ui-next/src/pages/definitions/rowColorHelpers.ts @@ -0,0 +1,24 @@ +type RowWithActive = { active?: boolean }; + +export const activeFilterGroups = [ + { title: "Active", value: "yes" }, + { title: "Inactive", value: "no" }, + { title: "Both", value: "all" }, +]; +// TODO this should be in the colors file. FIXME ask Leah! +export const pausedrowColor = "#949494"; +export const pausedLinkColor = "#619bd5"; +export const activeLinkColor = "#1976d2"; + +export const getLinkColor = (rec: RowWithActive) => + rec.active ? activeLinkColor : pausedLinkColor; + +export const conditionalRowStyles = [ + { + when: (row: RowWithActive) => row.active === false, + cl: "pausedrow", + style: { + color: `${pausedrowColor}`, + }, + }, +]; diff --git a/ui-next/src/pages/error/ErrorPage.tsx b/ui-next/src/pages/error/ErrorPage.tsx new file mode 100644 index 0000000000..744df97a8e --- /dev/null +++ b/ui-next/src/pages/error/ErrorPage.tsx @@ -0,0 +1,229 @@ +import Box from "@mui/material/Box"; +import MuiTypography from "components/MuiTypography"; +import Error from "components/v1/error/Error"; +import EmailNotVerifiedSvg from "images/svg/email-not-verified.svg"; +import UserNotFoundSvg from "images/svg/user-not-found.svg"; +import { useCallback, useMemo } from "react"; +import { Helmet } from "react-helmet"; +import { useQueryState } from "react-router-use-location-state"; +import { useAuth } from "shared/auth"; +import { + silentlyRefreshToken, + hasRefreshPermanentlyFailed, +} from "shared/auth/silentRefresh"; +import { canRefreshToken } from "shared/auth/tokenManagerJotai"; +import { HttpStatusCode } from "utils/constants/httpStatusCode"; +import { useErrorMonitoring } from "utils/monitoring"; +import Forbidden from "./Forbidden"; +import type { ParsedErrorMessage } from "./types"; +import { UserNotFound } from "./UserNotFound"; + +// @ts-ignore +const isUsingOkta = ["okta", "oidc"].includes(window?.authConfig?.type); + +const parseMessage = ({ + code, + error, + message, + onDone, +}: { + code: string; + error: string; + message: string; + onDone: () => void; + logOut: () => void; +}): ParsedErrorMessage => { + if (error === "EMAIL_NOT_VERIFIED") { + return { + code: "Please verify your email", + description: "I have already verified my email.", + title: "", + onClick: onDone, + buttonText: "Back To Login", + errorLogo: EmailNotVerifiedSvg, + }; + } + + if (error === "EXPIRED_TOKEN") { + return { + code: "EXPIRED TOKEN", + description: "Your session has expired.", + title: "EXPIRED TOKEN", + onClick: onDone, + buttonText: "REFRESH SESSION", + }; + } + + if (error === "USER_NOT_FOUND") { + return { + code: "Account does not exist", + description: + "Please notify your Orkes Cluster Admin to set up an account", + title: "User not found", + onClick: onDone, + buttonText: "Retry Login", + errorLogo: UserNotFoundSvg, + }; + } + + switch (Number(code)) { + case HttpStatusCode.Unauthorized: { + if (!isUsingOkta) { + return { + code, + description: + "Please logout and log back in. If the error persists, please clear your cache or force refresh the login page.", + title: "ERROR", + }; + } + + return { + code: "ACCESS DENIED", + description: + "This looks like a permission issue. Please contact your administrator for access.", + title: "ACCESS DENIED", + }; + } + + case HttpStatusCode.Forbidden: + return { + code: "ACCESS DENIED", + description: + "0AuthError: User is not assigned to the client application. Please contact your administrator.", + title: "ACCESS DENIED", + }; + + case HttpStatusCode.NotFound: + return { + code, + description: "We're sorry but we couldn't locate that page.", + title: "ERROR", + }; + + default: { + return { + code, + description: + message || + "Not sure what happened, but let's try again. If that doesn't work, let's restart the browser.", + title: "ERROR", + }; + } + } +}; + +export default function ErrorPage() { + const { solveExpireToken, logOut, oidcConfig } = useAuth(); + // const { useIdToken, acquireToken, setToken } = useAuth() as { + // useIdToken: string; + // acquireToken: (b: boolean) => void; + // setToken: (token?: string | null) => void; + // }; + const { notifyError } = useErrorMonitoring(); + + const [code] = useQueryState("code", `${HttpStatusCode.NotFound}`); + const [error] = useQueryState("error", ""); + const [message] = useQueryState("message", ""); + + notifyError("Unauthorized", { error, message }); + + const onDone = useCallback(async () => { + // if (useIdToken && acquireToken) { + // await acquireToken(true); + // } + // + // if (setToken) { + // // we set the token to null to force the OIDC flow to get a new token + // setToken(null); + // } + + // For 401 errors, try silent refresh first if possible + if (Number(code) === HttpStatusCode.Unauthorized) { + if (canRefreshToken() && !hasRefreshPermanentlyFailed()) { + const refreshed = await silentlyRefreshToken(oidcConfig); + + if (refreshed) { + window.location.replace("/"); + return; + } + } + } + + solveExpireToken(); + window.location.replace("/"); + }, [solveExpireToken, code, oidcConfig]); + + const parsedMessage = parseMessage({ code, error, message, onDone, logOut }); + + const ErrorComponent = useMemo(() => { + return () => { + if ( + error === "EXPIRED_TOKEN" || + Number(code) === HttpStatusCode.Forbidden + ) { + return ; + } else if (error === "USER_NOT_FOUND" || error === "EMAIL_NOT_VERIFIED") { + return ( + + ); + } else { + return ( + + ); + } + }; + }, [code, error, logOut, parsedMessage]); + + const pageTitle = useMemo(() => { + if (error === "EMAIL_NOT_VERIFIED") { + return "Email Not Verified"; + } + if (error === "USER_NOT_FOUND") { + return "User Not Found"; + } + return "Error"; + }, [error]); + + return ( + + + {pageTitle} + + {error === "USER_NOT_FOUND" ? ( + + + 401: + + {parsedMessage.title} + + ) : ( + + {parsedMessage.title} + + )} + + + + ); +} diff --git a/ui-next/src/pages/error/Forbidden.tsx b/ui-next/src/pages/error/Forbidden.tsx new file mode 100644 index 0000000000..07812fbad1 --- /dev/null +++ b/ui-next/src/pages/error/Forbidden.tsx @@ -0,0 +1,48 @@ +import { useAuth } from "shared/auth"; // TODO missing error page +import Error from "components/v1/error/Error"; +import useInterval from "utils/useInterval"; +import { tryToJson } from "utils/utils"; +import { ParsedErrorMessage } from "./types"; + +export interface ForbiddenProps { + parsedMessage: ParsedErrorMessage; +} + +export default function Forbidden({ parsedMessage }: ForbiddenProps) { + const { solveExpireToken } = useAuth(); + + // checking if client id is changed + useInterval(() => { + let invalidClientId = false; + + Object.entries(localStorage).forEach(([key, value]) => { + // checking auth0 key + const parsedValue = tryToJson(value); + if (key.startsWith("@@auth0spajs@@") && parsedValue !== undefined) { + const auth0Value = parsedValue; + + // @ts-ignore + if (auth0Value.body?.client_id !== window?.authConfig?.clientId) { + localStorage.removeItem(key); + + // re-fetch token + solveExpireToken(); + invalidClientId = true; + } + } + }); + + if (invalidClientId) { + window.location.replace("/"); + } + }, 1000); + + return ( + + ); +} diff --git a/ui-next/src/pages/error/UserNotFound.tsx b/ui-next/src/pages/error/UserNotFound.tsx new file mode 100644 index 0000000000..94e548273b --- /dev/null +++ b/ui-next/src/pages/error/UserNotFound.tsx @@ -0,0 +1,81 @@ +import { Box, Paper } from "@mui/material"; +import MuiButton from "components/MuiButton"; +import MuiTypography from "components/MuiTypography"; +import { ErrorProps } from "components/v1/error/Error"; +import { sidebarGrey } from "theme/tokens/colors"; + +export const UserNotFound = ({ + title, + description, + buttonText, + onClick, + errorLogo, + secondaryButton, +}: ErrorProps) => { + return ( + + userNotFound + + {title} + + + + + + {description} + + + {buttonText} + + {!!secondaryButton && ( + + {secondaryButton.buttonText} + + )} + + + + ); +}; diff --git a/ui-next/src/pages/error/types.ts b/ui-next/src/pages/error/types.ts new file mode 100644 index 0000000000..6e2b727104 --- /dev/null +++ b/ui-next/src/pages/error/types.ts @@ -0,0 +1,13 @@ +export interface ParsedErrorMessage { + code: string; + description: string; + title: string; + message?: string; + buttonText?: string; + onClick?: () => void; + errorLogo?: string; + secondaryButton?: { + buttonText?: string; + onClick?: () => void; + }; +} diff --git a/ui-next/src/pages/eventMonitor/EventMonitor.tsx b/ui-next/src/pages/eventMonitor/EventMonitor.tsx new file mode 100644 index 0000000000..c4083a3391 --- /dev/null +++ b/ui-next/src/pages/eventMonitor/EventMonitor.tsx @@ -0,0 +1,276 @@ +import { Box, Grid } from "@mui/material"; +import { Paper, NavLink } from "components"; +import { Helmet } from "react-helmet"; +import SectionContainer from "shared/SectionContainer"; +import SectionHeader from "shared/SectionHeader"; +import { useQueryState } from "react-router-use-location-state"; +import { useCallback, useMemo, useState } from "react"; +import DataTable from "components/DataTable/DataTable"; +import _isEmpty from "lodash/isEmpty"; +import { colors } from "theme/tokens/variables"; +import { EventExecutionDto } from "./types"; +import TagChip from "components/TagChip"; +import Header from "components/Header"; +import ConductorInput from "components/v1/ConductorInput"; +import useCustomPagination from "utils/hooks/useCustomPagination"; +import { LegacyColumn } from "components/DataTable/types"; +import { createTableTitle } from "utils/helpers"; +import ConductorMultiSelect from "components/v1/ConductorMultiSelect"; +import { RefreshEvent } from "./EventMonitorDetail/Refresher/RefreshEvent"; +import MuiTypography from "components/MuiTypography"; +import { useRefreshMachine } from "./EventMonitorDetail/Refresher/state/hook"; +import { PageType } from "./EventMonitorDetail/Refresher/state/types"; + +export const EVENT_STATUS_FILTER_QUERY_PARAM = "statusFilter"; + +const statusOptions: { name: string; label: string }[] = [ + { + label: "Active", + name: "ACTIVE", + }, + { + label: "Inactive", + name: "INACTIVE", + }, +]; + +const ActiveChip = ({ active }: { active: boolean }) => { + const color = active ? colors.successTag : colors.errorTag; + const chipStyles = + color == null + ? {} + : { + backgroundColor: color, + }; + + return ( + + ); +}; + +const columns: LegacyColumn[] = [ + { + id: "name", + name: "name", + label: "Name", + minWidth: "120px", + grow: 2, + tooltip: "Name of the event handler", + renderer: (name) => ( + {name} + ), + }, + { + id: "event", + name: "event", + label: "Event", + minWidth: "120px", + grow: 2, + tooltip: "Event field of the event handler definition", + }, + { + id: "active", + name: "active", + label: "Status", + grow: 2, + renderer: (active) => , + tooltip: "The status of the event", + }, + { + id: "numberOfMessages", + name: "numberOfMessages", + label: "Number of Messages", + tooltip: "Number of Messages received by the event handler", + right: true, + }, + { + id: "numberOfActions", + name: "numberOfActions", + label: "Number of Actions", + tooltip: "Number of Actions triggered by the event handler", + right: true, + }, +]; + +const StatusBadge = ({ label }: { label: string }) => { + const color = label === "Active" ? colors.successTag : colors.errorTag; + const chipStyles = + color == null + ? {} + : { + backgroundColor: color, + }; + + return ; +}; + +export const EventMonitor = () => { + const [ + { eventListData: data, isFetching, elapsed, refreshInterval, isError }, + { changeRefreshRate, handleRefresh }, + ] = useRefreshMachine(PageType.EVENT_LISTING); + const [ + { pageParam, searchParam }, + { handlePageChange, handleSearchTermChange }, + ] = useCustomPagination(); + + const [statusFilterParam, setStatusFilterParam] = useQueryState( + EVENT_STATUS_FILTER_QUERY_PARAM, + "", + ); + + const [statusFilter, setStatusFilter] = useState( + _isEmpty(statusFilterParam) ? [] : statusFilterParam.split(","), + ); + + const handleStatusChange = useCallback( + (values: string[]) => { + setStatusFilter(values); + setStatusFilterParam(values.join(",")); + }, + [setStatusFilter, setStatusFilterParam], + ); + + const checkStatusMatch = useCallback( + (x: EventExecutionDto) => { + if (statusFilter.length > 0) { + return statusFilter.includes(x.active ? "Active" : "Inactive"); + } + return true; + }, + [statusFilter], + ); + + const eventList = useMemo(() => { + let result: EventExecutionDto[] = []; + + if (data?.results) { + const lowerCaseSearchTerm = searchParam.toLowerCase(); + + result = data.results.filter( + (x) => + (x.name.toLowerCase()?.includes(lowerCaseSearchTerm) || + x.event.toLowerCase()?.includes(lowerCaseSearchTerm)) && + checkStatusMatch(x), + ); + } + return result; + }, [checkStatusMatch, data?.results, searchParam]); + + const selectedStatusDisplay = () => { + if ( + statusFilter.length === 0 || + statusFilter.length === statusOptions.length + ) { + return "All status"; + } + + return statusFilter.join(", "); + }; + + return ( + <> + + Event Monitor + + + + +
    + + + + + + + + status.label)} + onSelected={handleStatusChange} + value={statusFilter} + allText="All Status" + renderer={(option) => ( + + )} + /> + + + + + + + + {isError ? "Error while fetching events" : "No event found"} + + } + defaultShowColumns={[ + "name", + "event", + "active", + "numberOfMessages", + "numberOfActions", + ]} + columns={columns} + hideSearch + data={eventList} + title={true} + titleComponent={ + + {createTableTitle({ + filteredData: eventList, + data: data?.results ? data.results : [], + })} + + } + customActions={[ + + Showing status for : {selectedStatusDisplay()} + , + ]} + onChangePage={handlePageChange} + paginationDefaultPage={pageParam ? Number(pageParam) : 1} + /> + + + + ); +}; diff --git a/ui-next/src/pages/eventMonitor/EventMonitorDetail/EventMonitorDetail.tsx b/ui-next/src/pages/eventMonitor/EventMonitorDetail/EventMonitorDetail.tsx new file mode 100644 index 0000000000..be77be2d85 --- /dev/null +++ b/ui-next/src/pages/eventMonitor/EventMonitorDetail/EventMonitorDetail.tsx @@ -0,0 +1,403 @@ +import { Box, Grid, Stack } from "@mui/material"; +import { Button, DataTable, Paper } from "components"; +import { LegacyColumn } from "components/DataTable/types"; +import Header from "components/Header"; +import MuiButton from "components/MuiButton"; +import MuiTypography from "components/MuiTypography"; +import TagChip from "components/TagChip"; +import { ConductorAutoComplete } from "components/v1"; +import ConductorMultiSelect from "components/v1/ConductorMultiSelect"; +import XCloseIcon from "components/v1/icons/XCloseIcon"; +import { ConductorSectionHeader } from "components/v1/layout/section/ConductorSectionHeader"; +import _countBy from "lodash/countBy"; +import _get from "lodash/get"; +import _isEmpty from "lodash/isEmpty"; +import { useCallback, useEffect, useState } from "react"; +import { Helmet } from "react-helmet"; +import { Link, useParams } from "react-router"; +import { useQueryState } from "react-router-use-location-state"; +import SectionContainer from "shared/SectionContainer"; +import { EVENT_MONITOR_URL } from "utils/constants/route"; +import { EventItem, GroupedEventItem, ModalConfig } from "../types"; +import { + actions, + status, + statusConfig, + TIME_RANGE_OPTIONS, + truncatePayload, +} from "../utils"; +import { ExpandableGroupedItems, StatusBadge } from "./ExpandedGroupedItem"; +import { PayloadModal } from "./PayloadModal"; +import { RefreshEvent } from "./Refresher/RefreshEvent"; +import { useRefreshMachine } from "./Refresher/state/hook"; +import { PageType } from "./Refresher/state/types"; + +const StatusChips = ({ groupedItems }: { groupedItems?: EventItem[] }) => { + if (!groupedItems?.length) return N/A; + + const counts = _countBy(groupedItems, "status"); + + return ( + + {Object.entries(statusConfig) + .filter(([key]) => counts[key]) + .map(([key, config]) => ( + + ))} + + ); +}; + +const TruncatedPayload = ({ + payload, + title, + onViewMore, +}: { + payload: object; + title: string; + onViewMore: (payload: object, title: string) => void; +}) => { + return ( + + {truncatePayload(payload)} + {JSON.stringify(payload).length > 100 && ( + onViewMore(payload, title)} + > + View more + + )} + + ); +}; + +export const EventMonitorDetail = () => { + const params = useParams(); + const eventName = decodeURIComponent(_get(params, "name") || ""); + const [timeRange, setTimeRange] = useQueryState( + "from", + TIME_RANGE_OPTIONS[0]?.value.toString() || "", + ); + + const [ + { eventMonitorData, isFetching, elapsed, refreshInterval }, + { changeRefreshRate, handleRefresh }, + ] = useRefreshMachine(PageType.EVENT_DETAIL, eventName, parseInt(timeRange)); + + const [showPayloadModal, setShowPayloadModal] = useState(false); + const [modalConfig, setModalConfig] = useState(null); + + const handleOpenModal = useCallback( + (payload: EventItem, title = "Event Payload") => { + setModalConfig({ + payload, + title, + }); + setShowPayloadModal(true); + }, + [], + ); + + const handleCloseModal = useCallback(() => { + setShowPayloadModal(false); + setModalConfig(null); + }, []); + + const [actionFilterParam, setActionFilterParam] = useQueryState( + "actionFilter", + "", + ); + const [statusFilterParam, setStatusFilterParam] = useQueryState( + "statusFilter", + "", + ); + + const [actionFilter, setActionFilter] = useState(() => + _isEmpty(actionFilterParam) ? [] : actionFilterParam.split(","), + ); + + const [statusFilter, setStatusFilter] = useState(() => + _isEmpty(statusFilterParam) ? [] : statusFilterParam.split(","), + ); + + useEffect(() => { + const newActionFilterParam = actionFilter.join(","); + const newStatusFilterParam = statusFilter.join(","); + + if (newActionFilterParam !== actionFilterParam) { + setActionFilterParam(newActionFilterParam); + } + if (newStatusFilterParam !== statusFilterParam) { + setStatusFilterParam(newStatusFilterParam); + } + }, [ + actionFilter, + statusFilter, + actionFilterParam, + statusFilterParam, + setActionFilterParam, + setStatusFilterParam, + ]); + + const handleActionChange = useCallback( + (values: string[], type: "action" | "status") => { + if (type === "action") { + setActionFilter((prev) => + values.length === prev.length && values.every((v, i) => v === prev[i]) + ? prev + : values, + ); + } else if (type === "status") { + setStatusFilter((prev) => + values.length === prev.length && values.every((v, i) => v === prev[i]) + ? prev + : values, + ); + } + }, + [], + ); + + const filterEventData = useCallback( + (data: GroupedEventItem[]) => { + if (_isEmpty(actionFilter) && _isEmpty(statusFilter)) { + return data; + } + + return data.filter((item) => { + const groupedItems = item.groupedItems || []; + return groupedItems.some( + (groupItem) => + (_isEmpty(actionFilter) || + actionFilter.some( + (filter) => + filter.toLowerCase() === groupItem.action?.toLowerCase(), + )) && + (_isEmpty(statusFilter) || + statusFilter.some( + (filter) => + filter.toLowerCase() === groupItem.status?.toLowerCase(), + )), + ); + }); + }, + [actionFilter, statusFilter], + ); + const initColumns: LegacyColumn[] = [ + { + id: "messageId", + name: "messageId", + label: "Message Id", + }, + { + id: "payload", + name: "payload", + label: "Payload", + renderer: (val) => { + const title = "Payload"; + return ( + handleOpenModal(val, title)} + /> + ); + }, + }, + + { + id: "fullMessagePayload", + name: "fullMessagePayload", + label: "Full Message Payload", + minWidth: "220px", + renderer: (val) => { + const title = "Message Payload"; + return ( + handleOpenModal(val, title)} + /> + ); + }, + style: { + div: { + "&:first-child": { + overflow: "unset", + }, + }, + }, + }, + { + id: "status", + name: "status", + label: "Status", + right: true, + renderer: (_val, row) => , + }, + ]; + + return ( + + + Event Monitor Detail + + + +  Close + + } + /> + } + > + {showPayloadModal && modalConfig && ( + + )} +
    + + + + + action.name)} + multiple + freeSolo + onChange={(__, val: string[]) => + handleActionChange(val, "action") + } + value={actionFilter} + /> + + + + stat.name)} + onSelected={(values) => handleActionChange(values, "status")} + value={statusFilter} + allText="All Status" + renderer={(option) => } + /> + + + option.label)} + onChange={(__, val) => { + const selectedOption = TIME_RANGE_OPTIONS.find( + (option) => option.label === val, + ); + setTimeRange(selectedOption?.value.toString() || ""); + }} + value={ + TIME_RANGE_OPTIONS.find( + (option) => option.value.toString() === timeRange, + )?.label || null + } + isOptionEqualToValue={(option, currentValue) => + option === currentValue + } + clearIcon={false} + /> + + + + + + No action found for given event + + } + hideSearch + data={eventMonitorData ? filterEventData(eventMonitorData) : []} + columns={initColumns} + expandableRows + expandableRowDisabled={(row) => row.groups?.length <= 0} + expandableRowsComponent={({ data }) => ( + + )} + /> + + + + ); +}; diff --git a/ui-next/src/pages/eventMonitor/EventMonitorDetail/ExpandedGroupedItem.tsx b/ui-next/src/pages/eventMonitor/EventMonitorDetail/ExpandedGroupedItem.tsx new file mode 100644 index 0000000000..d105cd628b --- /dev/null +++ b/ui-next/src/pages/eventMonitor/EventMonitorDetail/ExpandedGroupedItem.tsx @@ -0,0 +1,129 @@ +import { Grid } from "@mui/material"; +import { Button, DataTable, NavLink } from "components"; +import { LegacyColumn } from "components/DataTable/types"; +import TagChip from "components/TagChip"; +import { useMemo } from "react"; +import { ExpanderComponentProps } from "react-data-table-component"; +import { colors } from "theme/tokens/variables"; +import { EventItem } from "../types"; +import { statusColors, status as statusOptions } from "../utils"; + +interface GroupedData { + groupedItems: EventItem[]; +} +interface ExpandableGroupedItemsProps extends ExpanderComponentProps { + actionFilter: string[]; + statusFilter: string[]; + onOpenModal: (payload: any) => void; +} + +export const StatusBadge = ({ status }: { status: string }) => { + const currentStatus = statusOptions.find((s) => s.name === status); + + const color = currentStatus + ? statusColors[currentStatus.name] + : colors.greyBorder; + const chipStyles = { + backgroundColor: color, + }; + + return ( + + ); +}; + +export const ExpandableGroupedItems = ({ + data, + actionFilter, + statusFilter, + onOpenModal, +}: ExpandableGroupedItemsProps) => { + const groupedColumns: LegacyColumn[] = [ + { + id: "action", + name: "action", + label: "Action", + sortable: false, + + renderer: (val, row) => ( + + ), + }, + + { + id: "status", + name: "status", + label: "status", + sortable: false, + grow: 0.5, + renderer: (val) => , + }, + { + id: "statusDescription", + name: "statusDescription", + label: "Status Description", + sortable: false, + style: { + div: { + "&:first-child": { + overflow: "unset", + }, + }, + }, + }, + { + id: "name", + name: "name", + label: "Event Handler", + sortable: false, + right: true, + renderer: (val) => ( + {val} + ), + }, + ]; + + const filteredItems = useMemo(() => { + return data.groupedItems.filter((item: EventItem) => { + const actionMatch = + actionFilter.length === 0 || + actionFilter.some( + (filter) => + filter.localeCompare(item.action, undefined, { + sensitivity: "base", + }) === 0, + ); + const statusMatch = + statusFilter.length === 0 || + statusFilter.some( + (filter) => + filter.localeCompare(item.status, undefined, { + sensitivity: "base", + }) === 0, + ); + return actionMatch && statusMatch; + }); + }, [data.groupedItems, actionFilter, statusFilter]); + return data ? ( + + + + + + ) : null; +}; diff --git a/ui-next/src/pages/eventMonitor/EventMonitorDetail/PayloadModal.tsx b/ui-next/src/pages/eventMonitor/EventMonitorDetail/PayloadModal.tsx new file mode 100644 index 0000000000..086e926477 --- /dev/null +++ b/ui-next/src/pages/eventMonitor/EventMonitorDetail/PayloadModal.tsx @@ -0,0 +1,100 @@ +import { + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Paper, +} from "@mui/material"; +import { Suspense } from "react"; +import { Editor } from "@monaco-editor/react"; +import MuiButton from "components/MuiButton"; +import { modalStyles } from "components/v1/Modal/commonStyles"; +import { Close, Code as CodeIcon } from "@mui/icons-material"; +import { defaultEditorOptions, type EditorOptions } from "shared/editor"; +const editorOption: EditorOptions = { + ...defaultEditorOptions, + tabSize: 2, + minimap: { enabled: false }, + quickSuggestions: true, + scrollbar: { + vertical: "auto", + verticalScrollbarSize: 8, + }, + formatOnType: true, + readOnly: true, + wordWrap: "on", + scrollBeyondLastLine: false, + automaticLayout: true, + fixedOverflowWidgets: true, +}; + +export const PayloadModal = ({ + payload, + handleClose, + title = "Event Payload", +}: { + payload: string; + handleClose: () => void; + title?: string; +}) => { + const onClose = ( + _event: Event, + reason: "backdropClick" | "escapeKeyDown" | "closeButtonClick", + ) => { + if (reason === "backdropClick") { + return false; + } + handleClose(); + }; + + return ( + <> + + + + {title} + + + + Loading...}> + + + + + + } + onClick={handleClose} + > + Close + + + + + ); +}; diff --git a/ui-next/src/pages/eventMonitor/EventMonitorDetail/Refresher/RefreshEvent.tsx b/ui-next/src/pages/eventMonitor/EventMonitorDetail/Refresher/RefreshEvent.tsx new file mode 100644 index 0000000000..99027c787f --- /dev/null +++ b/ui-next/src/pages/eventMonitor/EventMonitorDetail/Refresher/RefreshEvent.tsx @@ -0,0 +1,96 @@ +import { + Box, + CircularProgress, + FormControlLabel, + Grid, + Radio, + RadioGroup, +} from "@mui/material"; +import MuiTypography from "components/MuiTypography"; +import { Button } from "components"; +import { useMemo } from "react"; +import RefreshIcon from "components/v1/icons/RefreshIcon"; +const REFRESH_SECONDS_OPTIONS = [5, 10, 30, 60]; + +const getRefreshMessage = ( + isFetching: boolean, + refreshInterval: number, + elapsed: number, +) => { + if (isFetching) { + return "Refreshing..."; + } + return `Refresh in ${refreshInterval - elapsed}`; +}; + +export const RefreshEvent = ({ + refreshInterval, + isFetching, + elapsed, + handleRefresh, + changeRefreshRate, +}: { + refreshInterval: number; + isFetching: boolean; + elapsed: number; + handleRefresh: () => void; + changeRefreshRate: (val: number) => void; +}) => { + const startIcon = useMemo(() => { + return isFetching ? ( + + ) : ( + + ); + }, [isFetching]); + + return ( + + + + Refresh seconds + + + {REFRESH_SECONDS_OPTIONS.map((op) => ( + changeRefreshRate(op)} + checked={op === refreshInterval} + /> + } + label={op} + key={op} + /> + ))} + + + + + + + ); +}; diff --git a/ui-next/src/pages/eventMonitor/EventMonitorDetail/Refresher/state/actions.ts b/ui-next/src/pages/eventMonitor/EventMonitorDetail/Refresher/state/actions.ts new file mode 100644 index 0000000000..bda6aa2afe --- /dev/null +++ b/ui-next/src/pages/eventMonitor/EventMonitorDetail/Refresher/state/actions.ts @@ -0,0 +1,59 @@ +import { assign, DoneInvokeEvent } from "xstate"; +import { + PersistEventNameAndDuration, + RefreshMachineContext, + UpdateDurationEvent, +} from "./types"; +export const LOCAL_STORAGE_KEY = "eventMonitorRefreshSeconds"; + +const getStoredDuration = () => + parseInt(localStorage.getItem(LOCAL_STORAGE_KEY) || "60", 10); + +export const persistLocalStorageDuration = assign(() => { + const storedDuration = getStoredDuration(); + return { durationSet: storedDuration, duration: storedDuration }; +}); + +export const persistDuration = assign< + RefreshMachineContext, + UpdateDurationEvent +>({ + duration: (_, event) => { + const duration = event.value; + localStorage.setItem(LOCAL_STORAGE_KEY, `${duration}`); + + return duration; + }, + durationSet: (_, event) => event.value, +}); + +export const persistElapsed = assign({ + elapsed: (context) => context.elapsed + 1, +}); + +export const restartTimer = assign({ + duration: ({ durationSet }) => durationSet, + elapsed: 0, +}); + +export const persistEventData = assign< + RefreshMachineContext, + DoneInvokeEvent +>({ + eventData: (_, event) => event.data, +}); + +export const persistEventListData = assign< + RefreshMachineContext, + DoneInvokeEvent +>({ + eventListData: (_, event) => event.data, +}); + +export const persistEventNameAndTimer = assign< + RefreshMachineContext, + PersistEventNameAndDuration +>({ + eventName: (_, { data }) => data.eventName, + timeRange: (_, { data }) => data.timeRange, +}); diff --git a/ui-next/src/pages/eventMonitor/EventMonitorDetail/Refresher/state/hook.ts b/ui-next/src/pages/eventMonitor/EventMonitorDetail/Refresher/state/hook.ts new file mode 100644 index 0000000000..2313875354 --- /dev/null +++ b/ui-next/src/pages/eventMonitor/EventMonitorDetail/Refresher/state/hook.ts @@ -0,0 +1,107 @@ +import { useMachine, useSelector } from "@xstate/react"; +import { refreshMachine } from "./machine"; +import { State } from "xstate"; +import { + PageType, + RefreshMachineContext, + RefreshMachineEventTypes, + RefreshMachineStates, +} from "./types"; +import { useAuthHeaders } from "utils/query"; +import { useCallback, useContext, useEffect } from "react"; +import { MessageContext } from "components/v1/layout/MessageContext"; + +export const useRefreshMachine = ( + pageType?: PageType, + eventName?: string, + timeRange?: number, +) => { + const authHeaders = useAuthHeaders(); + const { setMessage } = useContext(MessageContext); + const [, send, service] = useMachine(refreshMachine, { + ...(process.env.NODE_ENV === "development" ? { devTools: true } : {}), + context: { + authHeaders, + pageType, + }, + actions: { + showErrorMessage: (__, { data }: any) => { + setMessage({ + text: data?.message ?? "Failed to fetch event monitor data", + severity: "error", + }); + }, + }, + }); + + const refreshInterval = useSelector( + service, + (state: State) => state.context.durationSet, + ); + + const eventMonitorData = useSelector( + service, + (state: State) => state.context.eventData, + ); + + const eventListData = useSelector( + service, + (state: State) => state.context.eventListData, + ); + + const isFetching = useSelector( + service, + (state: State) => + state.matches(RefreshMachineStates.FETCH_DATA) || + state.matches(RefreshMachineStates.FETCH_EVENT_LIST_DATA), + ); + const isError = useSelector(service, (state: State) => + state.matches(RefreshMachineStates.ERROR), + ); + + const elapsed = useSelector( + service, + (state: State) => state.context.elapsed, + ); + + const changeRefreshRate = (value: number) => { + send({ + type: RefreshMachineEventTypes.UPDATE_DURATION, + value, + }); + }; + const handleRefresh = () => + send({ + type: RefreshMachineEventTypes.REFRESH, + }); + + const persistEventNameAndTime = useCallback( + (eventName: string, timeRange: number) => + send({ + type: RefreshMachineEventTypes.PERSIST_EVENT_NAME_AND_TIME, + data: { + eventName, + timeRange, + }, + }), + [send], + ); + + useEffect(() => { + if (eventName && timeRange) { + persistEventNameAndTime(eventName, timeRange); + } + }, [eventName, persistEventNameAndTime, timeRange]); + + return [ + { + refreshInterval, + elapsed, + eventMonitorData, + isFetching, + eventListData, + isError, + }, + { changeRefreshRate, handleRefresh }, + ] as const; +}; diff --git a/ui-next/src/pages/eventMonitor/EventMonitorDetail/Refresher/state/machine.ts b/ui-next/src/pages/eventMonitor/EventMonitorDetail/Refresher/state/machine.ts new file mode 100644 index 0000000000..e24b7f1dd7 --- /dev/null +++ b/ui-next/src/pages/eventMonitor/EventMonitorDetail/Refresher/state/machine.ts @@ -0,0 +1,100 @@ +import { createMachine } from "xstate"; +import { + PageType, + RefreshMachineContext, + RefreshMachineEventTypes, + RefreshMachineStates, + TimerEvents, +} from "./types"; +import * as actions from "./actions"; +import * as services from "./services"; + +export const refreshMachine = createMachine( + { + id: "refreshMachine", + predictableActionArguments: true, + initial: RefreshMachineStates.INIT, + context: { + durationSet: 60, + elapsed: 0, + duration: 60, + pageType: undefined, + }, + on: { + [RefreshMachineEventTypes.UPDATE_DURATION]: { + actions: ["persistDuration", "restartTimer"], + }, + [RefreshMachineEventTypes.REFRESH]: { + actions: ["restartTimer"], + target: RefreshMachineStates.FETCH_DATA, + }, + [RefreshMachineEventTypes.PERSIST_EVENT_NAME_AND_TIME]: { + actions: ["persistEventNameAndTimer"], + target: RefreshMachineStates.FETCH_DATA, + }, + }, + states: { + [RefreshMachineStates.INIT]: { + entry: "persistLocalStorageDuration", + always: [ + { + cond: (ctx) => !ctx.eventData?.length, + target: RefreshMachineStates.FETCH_DATA, + }, + + { + target: RefreshMachineStates.RUNNING, + }, + ], + }, + [RefreshMachineStates.FETCH_DATA]: { + invoke: { + src: "fetchEventData", + onDone: [ + { + cond: (context) => context.pageType === PageType.EVENT_LISTING, + actions: ["persistEventListData", "restartTimer"], + target: RefreshMachineStates.RUNNING, + }, + { + actions: ["persistEventData", "restartTimer"], + target: RefreshMachineStates.RUNNING, + }, + ], + onError: { + actions: "showErrorMessage", + target: RefreshMachineStates.ERROR, + }, + }, + }, + [RefreshMachineStates.RUNNING]: { + on: { + [RefreshMachineEventTypes.TICK]: { + actions: "persistElapsed", + }, + }, + invoke: { + src: (_context) => (cb) => { + const interval = setInterval(() => { + cb(RefreshMachineEventTypes.TICK); + }, 1000); + + return () => { + clearInterval(interval); + }; + }, + }, + always: { + target: RefreshMachineStates.FETCH_DATA, + cond: (context: RefreshMachineContext) => + context.elapsed >= context.duration, + }, + }, + [RefreshMachineStates.ERROR]: {}, + }, + }, + { + actions: actions as any, + services: services as any, + }, +); diff --git a/ui-next/src/pages/eventMonitor/EventMonitorDetail/Refresher/state/services.ts b/ui-next/src/pages/eventMonitor/EventMonitorDetail/Refresher/state/services.ts new file mode 100644 index 0000000000..825c2c6d1a --- /dev/null +++ b/ui-next/src/pages/eventMonitor/EventMonitorDetail/Refresher/state/services.ts @@ -0,0 +1,67 @@ +import { queryClient } from "queryClient"; +import { PageType, RefreshMachineContext } from "./types"; +import { fetchContextNonHook, fetchWithContext } from "plugins/fetch"; +import { groupDataByMessageId } from "pages/eventMonitor/utils"; +import { AuthHeaders } from "types/common"; + +const fetchContext = fetchContextNonHook(); +const EVENT_MONITOR_URL = "event/execution"; + +const fetchEventMonitorData = async ( + headers: AuthHeaders, + eventName?: string, + timeRange?: number, +) => { + try { + if (eventName) { + let url = `event/execution/${eventName}`; + if (timeRange && timeRange > 0) { + url += `?from=${timeRange}`; + } + + const data = await queryClient.fetchQuery([fetchContext.stack, url], () => + fetchWithContext(url, fetchContext, { headers }), + ); + + return groupDataByMessageId(data); + } + } catch (error) { + console.error("Error fetching event monitor data:", error); + throw new Error("Failed to fetch event monitor data"); + } +}; + +const fetchEventListingData = async (headers: AuthHeaders) => { + try { + if (headers) { + const data = await queryClient.fetchQuery( + [fetchContext.stack, EVENT_MONITOR_URL], + () => fetchWithContext(EVENT_MONITOR_URL, fetchContext, { headers }), + ); + return data; + } + } catch (error) { + console.error("Error fetching event list data:", error); + throw new Error("Failed to fetch event list data"); + } +}; +export const fetchEventData = async ({ + pageType, + authHeaders, + eventName, + timeRange, +}: RefreshMachineContext) => { + if (!authHeaders) { + throw new Error("authHeaders is not defined"); + } + + try { + if (pageType === PageType.EVENT_LISTING) { + return await fetchEventListingData(authHeaders); + } + return await fetchEventMonitorData(authHeaders, eventName, timeRange); + } catch (error) { + console.error("Error fetching event data:", error); + throw new Error("Failed to fetch event data"); + } +}; diff --git a/ui-next/src/pages/eventMonitor/EventMonitorDetail/Refresher/state/types.ts b/ui-next/src/pages/eventMonitor/EventMonitorDetail/Refresher/state/types.ts new file mode 100644 index 0000000000..dc36e318b6 --- /dev/null +++ b/ui-next/src/pages/eventMonitor/EventMonitorDetail/Refresher/state/types.ts @@ -0,0 +1,65 @@ +import { + EventExecutionResult, + GroupedEventItem, +} from "pages/eventMonitor/types"; +import { AuthHeaders } from "types/common"; + +export enum PageType { + EVENT_LISTING = "EVENT_LISTING", + EVENT_DETAIL = "EVENT_DETAIL", +} + +export interface RefreshMachineContext { + elapsed: number; + duration: number; + durationSet: number; + authHeaders?: AuthHeaders; + eventData?: GroupedEventItem[]; + eventName?: string; + timeRange?: number; + pageType?: PageType; + eventListData?: EventExecutionResult; +} + +export enum RefreshMachineStates { + INIT = "INIT", + RUNNING = "RUNNING", + END_TIMER = "END_TIMER", + FETCH_DATA = "FETCH_DATA", + ERROR = "ERROR", + FETCH_EVENT_LIST_DATA = "FETCH_EVENT_LIST_DATA", +} + +export enum RefreshMachineEventTypes { + TICK = "TICK", + UPDATE_DURATION = "UPDATE_DURATION", + REFRESH = "REFRESH", + PERSIST_EVENT_NAME_AND_TIME = "PERSIST_EVENT_NAME_AND_TIME", +} + +export type TickEvent = { + type: RefreshMachineEventTypes.TICK; +}; + +export type RefreshEvent = { + type: RefreshMachineEventTypes.REFRESH; +}; + +export type UpdateDurationEvent = { + type: RefreshMachineEventTypes.UPDATE_DURATION; + value: number; +}; + +export type PersistEventNameAndDuration = { + type: RefreshMachineEventTypes.PERSIST_EVENT_NAME_AND_TIME; + data: { + eventName: string; + timeRange: number; + }; +}; + +export type TimerEvents = + | TickEvent + | UpdateDurationEvent + | RefreshEvent + | PersistEventNameAndDuration; diff --git a/ui-next/src/pages/eventMonitor/types.ts b/ui-next/src/pages/eventMonitor/types.ts new file mode 100644 index 0000000000..d2d9de540f --- /dev/null +++ b/ui-next/src/pages/eventMonitor/types.ts @@ -0,0 +1,43 @@ +export type EventExecutionDto = { + name: string; + event: string; + numberOfMessages: number; + numberOfActions: number; + active: boolean; +}; + +export type EventExecutionResult = { + results: EventExecutionDto[]; + totalHits: number; +}; + +export interface EventItem { + id: string; + messageId: string; + name: string; + event: string; + created: number; + status: string; + action: string; + payload: { + name: string; + }; + fullMessagePayload: { + _headers: Record; + name: string; + id: string; + message: string; + event: string; + _receiverHost: string | null; + }; + statusDescription: string; +} + +export interface GroupedEventItem extends EventItem { + groupedItems: EventItem[]; +} + +export type ModalConfig = { + payload: any; + title: string; +}; diff --git a/ui-next/src/pages/eventMonitor/utils.ts b/ui-next/src/pages/eventMonitor/utils.ts new file mode 100644 index 0000000000..5673563748 --- /dev/null +++ b/ui-next/src/pages/eventMonitor/utils.ts @@ -0,0 +1,83 @@ +import _groupBy from "lodash/groupBy"; +import { EventItem, GroupedEventItem } from "./types"; +import { colors } from "theme/tokens/variables"; + +export const groupDataByMessageId = (data: EventItem[]): GroupedEventItem[] => { + const groupedData = _groupBy(data, "messageId"); + + return Object.keys(groupedData).map((messageId) => ({ + ...groupedData[messageId][0], // Take the first item to copy over the base properties + groupedItems: groupedData[messageId], // Grouped items + })); +}; + +export const actions: { name: string; label: string }[] = [ + { + label: "Complete Task", + name: "COMPLETE_TASK", + }, + { + label: "Terminate Workflow", + name: "TERMINATE_WORKFLOW", + }, + { + label: "Update Variables", + name: "UPDATE_VARIABLES", + }, + { + label: "Fail Task", + name: "FAIL_TASK", + }, + { + label: "Start Workflow", + name: "START_WORKFLOW", + }, +]; + +export const status: { name: string; label: string }[] = [ + { + label: "In Progress", + name: "IN_PROGRESS", + }, + { + label: "Completed", + name: "COMPLETED", + }, + { + label: "Failed", + name: "FAILED", + }, + { + label: "Skipped", + name: "SKIPPED", + }, +]; + +export const statusColors: { [key: string]: string } = { + IN_PROGRESS: colors.progressTag, + COMPLETED: colors.successTag, + FAILED: colors.errorTag, + SKIPPED: colors.warningTag, +}; + +export const TIME_RANGE_OPTIONS = [ + { label: "5 minutes", value: 5 * 60 * 1000 }, + { label: "15 minutes", value: 15 * 60 * 1000 }, + { label: "30 minutes", value: 30 * 60 * 1000 }, + { label: "All time", value: -1 }, +]; + +export const statusConfig = { + FAILED: { label: "Failed", color: colors.errorTag }, + SKIPPED: { label: "Skipped", color: colors.greyBorder }, + IN_PROGRESS: { label: "In Progress", color: colors.progressTag }, + COMPLETED: { label: "Completed", color: colors.successTag }, +} as const; + +export const truncatePayload = (payload: object) => { + const jsonString = JSON.stringify(payload); + if (jsonString.length <= 100) { + return jsonString; + } + return jsonString.substring(0, 97) + "..."; +}; diff --git a/ui-next/src/pages/execution/ActionModule.jsx b/ui-next/src/pages/execution/ActionModule.jsx new file mode 100644 index 0000000000..28de14edbf --- /dev/null +++ b/ui-next/src/pages/execution/ActionModule.jsx @@ -0,0 +1,254 @@ +import { isFailedTask } from "utils"; +import { DropdownButton } from "components"; + +import { + ArrowCounterClockwise as ReplayIcon, + ArrowUUpLeft as RestartIcon, + Asterisk as RestartLatestIcon, + Pause as PauseIcon, + Play as ResumeIcon, + Stop as StopIcon, + ArrowSquareOut as RerunIcon, +} from "@phosphor-icons/react"; + +const style = { + menuIcon: { + marginRight: "10px", + }, +}; + +export default function ActionModule({ + execution, + onRestartExecutionWithLatestDefinitions, + onRestartExecutionWithCurrentDefinitions, + onRetryExecutionFromFailed, + onResumeExecution, + onTerminateExecution, + onPauseExecution, + onRetryResumeSubworkflow, + rerunExecutionWithLatestDefinitions, + createSheduleWithLatestDefinitions, +}) { + const { workflowDefinition } = execution; + + const { restartable } = workflowDefinition; // MOVE this cond + const rerunWorkflowOption = { + label: ( + <> + + Re-run workflow + + ), + handler: rerunExecutionWithLatestDefinitions, + }; + const createScheduleOption = { + label: ( + <> + + Create Schedule + + ), + handler: createSheduleWithLatestDefinitions, + }; + + // TODO build the options if no options grayout button + if (execution.status === "COMPLETED") { + const options = []; + if (restartable) { + options.push({ + label: ( + <> + + Restart with current definitions + + ), + handler: onRestartExecutionWithCurrentDefinitions, + }); + + options.push({ + label: ( + <> + + Restart with latest definitions + + ), + handler: onRestartExecutionWithLatestDefinitions, + }); + } + + options.push(rerunWorkflowOption); + options.push(createScheduleOption); + + return ( + + Actions + + ); + } else if (execution.status === "RUNNING") { + return ( + + + Terminate + + ), + handler: onTerminateExecution, + }, + { + label: ( + <> + + Pause + + ), + handler: onPauseExecution, + }, + rerunWorkflowOption, + ]} + > + Actions + + ); + } else if (execution.status === "PAUSED") { + return ( + + + Terminate + + ), + handler: onTerminateExecution, + }, + { + label: ( + <> + + Resume + + ), + handler: onResumeExecution, + }, + rerunWorkflowOption, + ]} + > + Actions + + ); + } else { + // FAILED, TIMED_OUT, TERMINATED + const options = []; + + if (["FAILED", "TIMED_OUT"].includes(execution.status)) { + options.push({ + label: ( + <> + + Terminate + + ), + handler: onTerminateExecution, + }); + } + + if (restartable) { + options.push({ + label: ( + <> + + Restart with current definitions + + ), + handler: onRestartExecutionWithCurrentDefinitions, + }); + + options.push({ + label: ( + <> + + Restart with latest definitions + + ), + handler: onRestartExecutionWithLatestDefinitions, + }); + } + + if ( + execution?.tasks?.some( + (task) => !task.retried && isFailedTask(task.status), + ) + ) { + options.push({ + label: ( + <> + + Retry - from failed task + + ), + handler: onRetryExecutionFromFailed, + }); + } + + options.push(rerunWorkflowOption); + + if ( + (execution.status === "FAILED" || execution.status === "TIMED_OUT") && + execution.tasks.find( + (task) => + task.workflowTask.type === "SUB_WORKFLOW" && + isFailedTask(task.status), + ) + ) { + options.push({ + label: ( + <> + + Retry - resume subworkflow + + ), + handler: onRetryResumeSubworkflow, + }); + } + + return ( + + Actions + + ); + } +} diff --git a/ui-next/src/pages/execution/Execution.jsx b/ui-next/src/pages/execution/Execution.jsx new file mode 100644 index 0000000000..fcaaa6b6dd --- /dev/null +++ b/ui-next/src/pages/execution/Execution.jsx @@ -0,0 +1,718 @@ +import LaunchIcon from "@mui/icons-material/Launch"; +import { Box, Stack, Tooltip } from "@mui/material"; +import { AutoRefreshButton, Button, Heading, LinearProgress } from "components"; +import MuiAlert from "components/MuiAlert"; +import MuiTypography from "components/MuiTypography"; +import NavLink from "components/NavLink"; +import { SnackbarMessage } from "components/SnackbarMessage"; +import StatusBadge from "components/StatusBadge"; +import TwoPanesDivider from "components/TwoPanesDivider"; +import { Flow } from "components/flow/Flow"; +import { CopyClipboardButton } from "components/v1/CopyClipboardButton"; +import OpenIcon from "components/v1/icons/OpenIcon"; +import ButtonLinks from "components/v1/layout/header/ButtonLinks"; +import { ConductorSectionHeader } from "components/v1/layout/section/ConductorSectionHeader"; +import { SidebarContext } from "components/Sidebar/context/SidebarContext"; +import { path as _path } from "lodash/fp"; +import { useContext, useMemo } from "react"; +import { Helmet } from "react-helmet"; +import { useLocation } from "react-router"; +import { colors } from "theme/tokens/variables"; +import { WorkflowExecutionStatus } from "types/Execution"; +import { TaskStatus } from "types/TaskStatus"; +import { openInNewTab } from "utils/helpers"; +import { usePushHistory } from "utils/hooks/usePushHistory"; +import ActionModule from "./ActionModule"; +import InputOutput from "./ExecutionInputOutput"; +import ExecutionJson from "./ExecutionJson"; +import ExecutionSummary from "./ExecutionSummary"; +import LeftPanelTabs from "./LeftPanelTabs"; +import { RightPanel } from "./RightPanel"; +import { TaskList } from "./TaskList/TaskList"; +import Timeline from "./Timeline"; +import { FlowExecutionContextProvider } from "./state"; +import { useExecutionMachine } from "./state/hook"; +import { ExecutionTabs } from "./state/types"; +import { WorkflowIntrospection } from "pages/execution/WorkflowIntrospection"; +import Agent from "components/agent/Agent"; +import { AgentDisplayMode } from "components/agent/agent-types"; +import { agentFirstUseAtom } from "shared/agent/agentAtomsStore"; +import { useAtom } from "jotai"; + +const SecondaryActions = ({ + execution, + countdownActor, + onRestartExecutionWithLatestDefinitions, + onRestartExecutionWithCurrentDefinitions, + onRetryExecutionFromFailed, + onResumeExecution, + onTerminateExecution, + onPauseExecution, + onRetryResumeSubworkflow, + rerunExecutionWithLatestDefinitions, + createSheduleWithLatestDefinitions, + refetch, +}) => { + const isDynamic = _path("input._systemMetadata.dynamic", execution); + return ( + execution && ( + + {execution.parentWorkflowId && ( + + )} + + + {isDynamic ? null : ( + + + + )} + + + + + ) + ); +}; + +const FailureAlert = ({ failedWFLink, alertText }) => { + const navigate = usePushHistory(); + + const alertStyle = { + padding: "0 10px", + fontSize: "12px", + height: "28px", + width: "fit-content", + fontWeight: "500", + cursor: "pointer", + marginRight: "10px", + ".MuiAlert-message, .css-1ytlwq5-MuiAlert-icon": { + padding: "4px 0px", + }, + ".css-1ytlwq5-MuiAlert-icon": { marginRight: "8px" }, + "&:hover": { + border: "1px solid #badfff", + }, + alignItems: "center", + }; + + return ( + navigate(`/execution/${failedWFLink}`)} + > + {alertText} + + ); +}; + +const ReasonForIncompletion = ({ reason, navigate, location }) => { + if (!reason) return null; + + if (reason.length >= 300) { + return ( + + {reason.substr(0, 60)}... [ + { + navigate(`${location.pathname}?tab=summary`); + }} + > + View full message + + ] + + ); + } + + return <>{reason}; +}; + +const ExecutionAlert = ({ + execution, + openedTab, + failedTaskWithReason, + handleJumpToTask, +}) => { + const navigate = usePushHistory(); + const location = useLocation(); + + if ( + execution?.rateLimited || + (execution?.reasonForIncompletion && + execution?.status !== WorkflowExecutionStatus.COMPLETED) + ) { + return ( + + + {execution?.rateLimited ? ( + "This execution is rate limited and will be executed once previous executions are completed." + ) : ( + + )} + + + {openedTab === ExecutionTabs.DIAGRAM_TAB && failedTaskWithReason && ( + + Jump to task + + )} + + ); + } + return null; +}; + +export default function Execution() { + const [ + { + selectTask, + expandDynamic, + collapseDynamic, + clearError, + rerunExecutionWithLatestDefinitions, + createSheduleWithLatestDefinitions, + restartExecutionWithLatestDefinitions, + restartExecutionWithCurrentDefinitions, + retryExcutionFromFailed, + retryResumeSubworkflow, + resumeExecution, + terminateExecution, + pauseExecution, + changeExecutionTab, + closeRightPanel, + refetch, + handleUpdateVariables, + selectNode, + toggleAssistantPanel, + }, + { + flowActor, + execution, + executionId, + isReady, + selectedTask, + executionStatusMap, + countdownActor, + maybeError, + maybeMessage, + openedTab, + taskListActor, + rightPanelActor, + isNoAccess, + doWhileSelection, + nodes, + isAssistantPanelOpen, + }, + ] = useExecutionMachine(); + const location = useLocation(); + + const { open: isSideBarOpen } = useContext(SidebarContext); + + const [, setAgentFirstUse] = useAtom(agentFirstUseAtom); + + const isFailure = (workflow) => { + const workflowInput = workflow?.input; + if ( + workflowInput?.reason && + workflowInput?.failureTaskId && + workflowInput?.workflowId && + workflowInput?.failureStatus + ) { + return workflow.input.workflowId; + } + }; + + const isExecutionView = location.pathname.startsWith("/execution/"); + + const failureWorkflowId = isFailure(execution); + + const leftPanelContent = ( + <> + {execution && ( + + <> + {openedTab === ExecutionTabs.DIAGRAM_TAB && + execution && + flowActor && ( + + + + )} + {openedTab === ExecutionTabs.TASK_LIST_TAB && taskListActor && ( + + )} + {openedTab === ExecutionTabs.TIMELINE_TAB && ( + + )} + {openedTab === ExecutionTabs.WORKFLOW_INTROSPECTION && ( + + )} + {openedTab === ExecutionTabs.SUMMARY_TAB && ( + + )} + {openedTab === ExecutionTabs.WORKFLOW_INPUT_OUTPUT_TAB && ( + + )} + {openedTab === ExecutionTabs.JSON_TAB && ( + + )} + {openedTab === ExecutionTabs.VARIABLES_TAB && ( + + )} + {openedTab === ExecutionTabs.TASKS_TO_DOMAIN_TAB && ( + + )} + + + )} + + ); + + const rightPanelContent = ( + <> + {isAssistantPanelOpen ? ( + + theme.palette?.mode === "dark" ? colors.gray14 : undefined, + backgroundColor: (theme) => + theme.palette?.mode === "dark" ? colors.gray01 : undefined, + }} + > + + + ) : ( + rightPanelActor && ( + + theme.palette?.mode === "dark" ? colors.gray14 : undefined, + backgroundColor: (theme) => + theme.palette?.mode === "dark" ? colors.gray01 : undefined, + }} + > + + + ) + )} + + ); + + const workflowTitle = useMemo( + () => (execution?.workflowType || execution?.workflowName) ?? null, + [execution?.workflowType, execution?.workflowName], + ); + + const failedTaskWithReason = useMemo( + () => + execution?.tasks?.find( + (task) => + task?.status === TaskStatus.FAILED && task?.reasonForIncompletion, + ), + [execution?.tasks], + ); + + const handleJumpToTask = () => { + const maybeSelectedNode = nodes?.find( + (node) => node?.id === failedTaskWithReason?.referenceTaskName, + ); + if (maybeSelectedNode) { + selectNode(maybeSelectedNode); + } + }; + + return ( + + + + Execution - {workflowTitle === null ? "" : workflowTitle} -{" "} + {executionId} + + + + + + {!isReady && !isNoAccess && } + + {execution && ( + + + + + + {workflowTitle} + + + + + + + } + breadcrumbItems={[ + { label: "Workflow Executions", to: "/executions" }, + { + label: execution.workflowId || "", + to: "", + icon: , + }, + ]} + buttonsComponent={ + + {failureWorkflowId && ( + + )} + + {execution?.output?.["conductor.failure_workflow"] && ( + + )} + + + + + + } + /> + + + + { + setAgentFirstUse(true); + toggleAssistantPanel(); + }} + /> + + + )} + + + + + + + + + ); +} diff --git a/ui-next/src/pages/execution/ExecutionInputOutput.tsx b/ui-next/src/pages/execution/ExecutionInputOutput.tsx new file mode 100644 index 0000000000..8b930ca147 --- /dev/null +++ b/ui-next/src/pages/execution/ExecutionInputOutput.tsx @@ -0,0 +1,128 @@ +import GridViewOutlinedIcon from "@mui/icons-material/GridViewOutlined"; +import ListOutlinedIcon from "@mui/icons-material/ListOutlined"; +import Box from "@mui/material/Box"; +import Grid from "@mui/material/Grid"; +import Stack from "@mui/material/Stack"; +import { CSSProperties, useState } from "react"; + +import { ReactJson } from "components"; +import MuiIconButton from "components/MuiIconButton"; +import { colors } from "theme/tokens/variables"; + +type DataType = { + title: string; + src: Record; + hidden: boolean; + style: CSSProperties; +}; + +interface InputOutputProp { + data: DataType[]; + execution: Record; + isEditable?: boolean; + handleUpdate?: (value: string) => void; +} + +export default function InputOutput({ + data, + execution, + isEditable = false, + handleUpdate, +}: InputOutputProp) { + const [isDisplayList, setIsDisplayList] = useState(false); + const [fullScreen, setFullScreen] = useState([]); + + const handleFullScreen = (item: DataType) => { + if (fullScreen.length > 0) { + setFullScreen([]); + return; + } + setFullScreen([item]); + }; + + const customEditorOptions = { + minimap: { enabled: false }, + lightbulb: { enabled: false }, + renderLineHighlight: "none", + overviewRulerLanes: 0, + hideCursorInOverviewRuler: true, + scrollbar: { + // this property is added because it was not allowing us to scroll when mouse pointer is over this component + alwaysConsumeMouseWheel: false, + verticalSliderSize: 9, + horizontalSliderSize: 9, + useShadows: true, + }, + }; + + const renderItems = (items: DataType[]) => { + return items.map((item, index) => + item.hidden ? null : ( + + + + + + ), + ); + }; + + const isManyItems = data.length > 1; + + return ( + <> + {isManyItems && ( + + + setIsDisplayList(true)}> + + + setIsDisplayList(false)}> + + + + + )} + + + {renderItems(fullScreen.length > 0 ? fullScreen : data)} + + + ); +} diff --git a/ui-next/src/pages/execution/ExecutionJson.jsx b/ui-next/src/pages/execution/ExecutionJson.jsx new file mode 100644 index 0000000000..143791028f --- /dev/null +++ b/ui-next/src/pages/execution/ExecutionJson.jsx @@ -0,0 +1,42 @@ +import { Box } from "@mui/material"; +import ReactJson from "components/ReactJson"; + +const EDITOR_THEME_KEY = "editorTheme"; + +const getReactJsonTheme = () => { + let localEditorTheme = localStorage.getItem(EDITOR_THEME_KEY); + if (!localEditorTheme) { + localEditorTheme = "vs-light"; + } + let theme = "shapeshifter"; + if (localEditorTheme === "vs-light") { + theme = "shapeshifter:inverted"; + } + return theme; +}; + +export default function ExecutionJson({ execution }) { + const theme = getReactJsonTheme(); + return ( + + + + ); +} diff --git a/ui-next/src/pages/execution/ExecutionSummary.jsx b/ui-next/src/pages/execution/ExecutionSummary.jsx new file mode 100644 index 0000000000..05d0d6a410 --- /dev/null +++ b/ui-next/src/pages/execution/ExecutionSummary.jsx @@ -0,0 +1,75 @@ +import { KeyValueTable, NavLink, Paper } from "components"; + +const style = { + paper: { + minHeight: 300, + }, +}; + +export default function ExecutionSummary({ execution }) { + // To accommodate unexecuted tasks, read type & name out of workflowTask + const data = [ + { label: "Workflow id", value: execution.workflowId }, + { label: "Status", value: execution.status, type: "status" }, + { label: "Version", value: execution.workflowVersion }, + { label: "Start time", value: execution.startTime, type: "date" }, + { label: "End time", value: execution.endTime, type: "date" }, + { + label: "Duration", + value: execution.endTime - execution.startTime, + type: "duration", + }, + ]; + + if (execution.parentWorkflowId) { + data.push({ + label: "Parent workflow id", + value: ( + + {execution.parentWorkflowId} + + ), + }); + } + + if (execution.parentWorkflowTaskId) { + data.push({ + label: "Parent task id", + value: execution.parentWorkflowTaskId, + }); + } + + if (execution.reasonForIncompletion) { + const statusIndex = data.findIndex((item) => item.label === "Status"); + data.splice(statusIndex + 1, 0, { + label: "Reason for incompletion", + value: execution.reasonForIncompletion, + type: "error", + }); + } + + if (execution.correlationId) { + data.push({ + label: "Correlation id", + value: execution.correlationId, + }); + } + if (execution.idempotencyKey) { + data.push({ + label: "Idempotency key", + value: execution.idempotencyKey, + }); + } + if (execution.event) { + data.push({ + label: "Trigger event", + value: execution.event, + }); + } + + return ( + + + + ); +} diff --git a/ui-next/src/pages/execution/LeftPanelTabs.tsx b/ui-next/src/pages/execution/LeftPanelTabs.tsx new file mode 100644 index 0000000000..7d07047aa9 --- /dev/null +++ b/ui-next/src/pages/execution/LeftPanelTabs.tsx @@ -0,0 +1,123 @@ +import { Tab, Tabs } from "components"; +import { ExecutionTabs } from "./state/types"; +import { WorkflowExecution } from "types/Execution"; +import { featureFlags, FEATURES } from "utils/flags"; +import { agentFirstUseAtom } from "shared/agent/agentAtomsStore"; +import { useAtom } from "jotai"; + +export interface LeftPanelTabsProps { + execution: WorkflowExecution; + openedTab: boolean; + onChangeExecutionTab: (tab: ExecutionTabs) => void; + onToggleAssistant: () => void; +} + +const isWorkflowIntrospectionEnabled = featureFlags.isEnabled( + FEATURES.WORKFLOW_INTROSPECTION, +); + +const showAgent = featureFlags.isEnabled(FEATURES.SHOW_AGENT); + +export default function LeftPanelTabs({ + openedTab, + onChangeExecutionTab, + onToggleAssistant, +}: LeftPanelTabsProps) { + const [firstUse] = useAtom(agentFirstUseAtom); + + const leftPanelTabItems = [ + { + label: "Diagram", + onClick: () => onChangeExecutionTab(ExecutionTabs.DIAGRAM_TAB), + value: ExecutionTabs.DIAGRAM_TAB, + }, + { + label: "Task List", + onClick: () => onChangeExecutionTab(ExecutionTabs.TASK_LIST_TAB), + value: ExecutionTabs.TASK_LIST_TAB, + }, + { + label: "Timeline", + onClick: () => onChangeExecutionTab(ExecutionTabs.TIMELINE_TAB), + value: ExecutionTabs.TIMELINE_TAB, + }, + { + label: "Summary", + onClick: () => onChangeExecutionTab(ExecutionTabs.SUMMARY_TAB), + value: ExecutionTabs.SUMMARY_TAB, + }, + { + label: "Workflow Input/Output", + onClick: () => + onChangeExecutionTab(ExecutionTabs.WORKFLOW_INPUT_OUTPUT_TAB), + value: ExecutionTabs.WORKFLOW_INPUT_OUTPUT_TAB, + }, + { + label: "JSON", + onClick: () => onChangeExecutionTab(ExecutionTabs.JSON_TAB), + value: ExecutionTabs.JSON_TAB, + }, + { + label: "Variables", + onClick: () => onChangeExecutionTab(ExecutionTabs.VARIABLES_TAB), + value: ExecutionTabs.VARIABLES_TAB, + }, + { + label: "Tasks to Domain", + onClick: () => onChangeExecutionTab(ExecutionTabs.TASKS_TO_DOMAIN_TAB), + value: ExecutionTabs.TASKS_TO_DOMAIN_TAB, + }, + ...(showAgent + ? [ + { + label: "Assistant", + onClick: onToggleAssistant, + value: null, + tabSx: { + animation: !firstUse + ? "rotate-color 3s ease-in-out infinite" + : "none", + "@keyframes rotate-color": { + "0%, 100%": { + color: "rgba(36, 157, 233, 0.74)", + }, + "50%": { + color: "rgba(212, 13, 219, 0.74)", + }, + }, + }, + }, + ] + : []), + ]; + + // Add Workflow Introspection tab only if the feature flag is enabled + if (isWorkflowIntrospectionEnabled) { + leftPanelTabItems.splice(3 /* After the timeline tab */, 0, { + label: "Workflow Introspection", + onClick: () => onChangeExecutionTab(ExecutionTabs.WORKFLOW_INTROSPECTION), + value: ExecutionTabs.WORKFLOW_INTROSPECTION, + }); + } + + return ( + + {leftPanelTabItems.map(({ label, onClick, value, tabSx }) => ( + + ))} + + ); +} diff --git a/ui-next/src/pages/execution/NoAnimRangeSlider.tsx b/ui-next/src/pages/execution/NoAnimRangeSlider.tsx new file mode 100644 index 0000000000..544fe5731c --- /dev/null +++ b/ui-next/src/pages/execution/NoAnimRangeSlider.tsx @@ -0,0 +1,45 @@ +import Slider from "@mui/material/Slider"; +import { styled } from "@mui/material/styles"; +import _nth from "lodash/nth"; + +const CustomSlider = styled(Slider)((props) => { + // used for switching the tooltip/label position + const min = props.min ?? 0; + const max = props.max ?? 0; + const mid = (min + max) / 2; + const currentMinVal = _nth(props?.value as number[], 0) ?? min; + const currentMaxVal = _nth(props?.value as number[], 1) ?? max; + // + return { + "& .MuiSlider-thumb": { + transition: "none", + }, + "& .MuiSlider-track": { + transition: "none", + }, + + "& .MuiSlider-markLabel": { + color: "primary.contrastText", + fontSize: "0.75rem", + }, + '&.MuiSlider-root .MuiSlider-thumb[data-index="0"] .MuiSlider-valueLabel': { + ...(currentMinVal < mid + ? { marginLeft: "166px" } + : { marginRight: "166px" }), + + "&::before": { + left: currentMinVal < mid ? "12px" : "calc(100% - 12px)", + }, + }, + '&.MuiSlider-root .MuiSlider-thumb[data-index="1"] .MuiSlider-valueLabel': { + ...(currentMaxVal > mid + ? { marginRight: "166px" } + : { marginLeft: "166px" }), + "&::before": { + left: currentMaxVal > mid ? "calc(100% - 12px)" : "12px", + }, + }, + }; +}); + +export default CustomSlider; diff --git a/ui-next/src/pages/execution/RightPanel/RightPanel.tsx b/ui-next/src/pages/execution/RightPanel/RightPanel.tsx new file mode 100644 index 0000000000..0e28463818 --- /dev/null +++ b/ui-next/src/pages/execution/RightPanel/RightPanel.tsx @@ -0,0 +1,683 @@ +import { Box, Paper } from "@mui/material"; +import { ArrowCounterClockwise, X as CloseIcon } from "@phosphor-icons/react"; +import { + Button, + DropdownButton, + Heading, + IconButton, + ReactJson, + Tab, + Tabs, +} from "components"; +import ClipboardCopy from "components/ClipboardCopy"; +import { dowhileHasAllIterationsInOutput } from "components/flow/components/shapes/TaskCard/helpers"; +import { SnackbarMessage } from "components/SnackbarMessage"; +import StatusBadge from "components/StatusBadge"; +import ConductorTooltip from "components/conductorTooltip/ConductorTooltip"; +import _nth from "lodash/nth"; +import { FunctionComponent, useMemo } from "react"; +import { useContainerQuery } from "react-container-query"; +import { colors } from "theme/tokens/variables"; +import { TaskType } from "types/common"; +import { + DoWhileSelection, + ExecutionTask, + WorkflowExecutionStatus, +} from "types/Execution"; +import { TaskStatus } from "types/TaskStatus"; +import { featureFlags, FEATURES } from "utils/flags"; +import { usePushHistory } from "utils/hooks/usePushHistory"; +import { ActorRef } from "xstate"; +import { UpdateTaskStatusForm } from ".."; +import { + DEFINITION_TAB, + INPUT_TAB, + JSON_TAB, + LOGS_TAB, + OUTPUT_TAB, + SUMMARY_TAB, +} from "../state/constants"; +import TaskLogs from "../TaskLogs"; +import TaskSummary from "../TaskSummary"; +import { RightPanelContextEventTypes, RightPanelEvents } from "./state"; +import { useRightPanelActor } from "./state/hook"; +import { SummaryTask } from "./SummaryTask"; + +const executionTaskHeaderContainerQuery = { + small: { + maxWidth: 699, + }, + large: { + minWidth: 700, + }, +}; + +const rerunFromForkAndDowhileTasksEnabled = featureFlags.isEnabled( + FEATURES.ENABLE_RERUN_FROM_FORK_AND_DOWHILE_TASKS, +); + +interface SecondaryActionsProps { + selectedTask: ExecutionTask; + containerQueryState: any; + dynamicForkInstances: any; +} +interface LabelRendererProps { + iterationTask: any; + isIteration?: boolean; + hideTaskId?: boolean; +} + +interface RightPanelProps { + rightPanelActor: ActorRef; + workflowName: string; + workflowStatus: string; + doWhileSelection?: DoWhileSelection[]; +} + +const SecondaryActions = ({ + selectedTask, + containerQueryState, + dynamicForkInstances, +}: SecondaryActionsProps) => { + const navigate = usePushHistory(); + return selectedTask?.workflowTask?.type === "SIMPLE" ? ( // Within dynamic forks. Tasks cant be simple. the task name is used as the type. since the name is generated + + + + + + ) : ( + dynamicForkInstances + ); +}; + +const LabelRenderer = ({ + iterationTask, + isIteration, + hideTaskId = false, +}: LabelRendererProps) => { + const textLabel = isIteration + ? `Iteration ${iterationTask.iteration} ${ + iterationTask?.retryCount > 0 + ? " - retry attempt " + iterationTask.retryCount + : "" + }` + : `Attempt #${iterationTask.retryCount}`; + return ( + + {dropdownIcon(iterationTask.status)}{" "} + {`${textLabel} ${hideTaskId ? "" : `- ${iterationTask.taskId}`}`} + + ); +}; +function getOrderedIterationKeys( + outputData: Record, + selectedTask: { iteration?: number }, +): number[] { + // Extract keys that are numbers + const keys = Object.keys(outputData) + .map(Number) + .filter((k) => !isNaN(k)); + // Sort numerically + keys.sort((a, b) => b - a); + + // Prepend the selected iteration if it is not in the list + if ( + typeof selectedTask.iteration === "number" && + !keys.includes(selectedTask.iteration) + ) { + return [selectedTask.iteration, ...keys]; + } + return keys; +} + +const DoWhileIteration = ({ + selectedTask, + handleSelectDoWhileIteration, + doWhileSelection, +}: { + selectedTask: ExecutionTask; + handleSelectDoWhileIteration: (data: DoWhileSelection) => void; + doWhileSelection?: DoWhileSelection[]; +}) => { + const isTaskProcessing = [ + TaskStatus.PENDING, + TaskStatus.SCHEDULED, + TaskStatus.IN_PROGRESS, + ].includes(selectedTask.status); + const retryIterationOptions = getOrderedIterationKeys( + selectedTask?.outputData ?? {}, + selectedTask, + ); + + const currentIteration = _nth( + doWhileSelection?.filter( + (item) => + item.doWhileTaskReferenceName === selectedTask?.referenceTaskName, + ), + 0, + )?.selectedIteration; + + const dropIconRender = (option: number) => { + const completedIterations = Object.keys(selectedTask?.outputData ?? {}); + if (completedIterations.includes(option.toString())) { + return dropdownIcon("COMPLETED"); + } + return dropdownIcon(selectedTask.status); + }; + + return ( + + + { + return { + label: ( + + {dropIconRender(option)} {`iteration ${option}`} + + ), + handler: () => + handleSelectDoWhileIteration({ + doWhileTaskReferenceName: selectedTask?.referenceTaskName, + selectedIteration: option, + }), + }; + }) ?? [] + } + > + + {currentIteration != null ? ( + <> + {dropIconRender(currentIteration)}{" "} + {`iteration ${currentIteration}`} + + ) : null} + + + {selectedTask?.inputData?.keepLastN != null ? ( + + info + + ) : !isTaskProcessing && + !dowhileHasAllIterationsInOutput(selectedTask?.outputData ?? {}) ? ( + + info + + ) : null} + + + ); +}; + +export const RightPanel: FunctionComponent = ({ + rightPanelActor, + workflowName, + workflowStatus, + doWhileSelection, +}) => { + const [containerQueryState, containerRef] = useContainerQuery( + executionTaskHeaderContainerQuery, + { width: 100, height: 100 }, + ); + + const [ + { + selectedTask, + isIteration, + retryIterationOptions, + errorMessage, + currentTab, + maybeSiblings, + isReRunFromTaskInProgress, + }, + { + handleChangeTaskStatus: onChangeTaskStatus, + handleClosePanel: onClosePanel, + handleReRunRequest, + clearErrorMessage, + handleSelectTask, + handleSelectDoWhileIteration, + }, + ] = useRightPanelActor(rightPanelActor); + + const dfOptions: ExecutionTask[] = maybeSiblings; + + const maybeStatusForm = useMemo( + () => + selectedTask?.status && + [TaskStatus.IN_PROGRESS, TaskStatus.SCHEDULED].includes( + selectedTask.status, + ) ? ( + + ) : null, + [selectedTask, onChangeTaskStatus], + ); + + const maybeRerunTask = useMemo(() => { + if (workflowStatus !== WorkflowExecutionStatus.PAUSED) { + return ( + + + + ); + } + return null; + }, [handleReRunRequest, workflowStatus]); + + const changeCurrentTab = (tab: number) => { + rightPanelActor.send({ + type: RightPanelContextEventTypes.CHANGE_CURRENT_TAB, + currentTab: tab, + }); + }; + + // If the summary task is selected just show a small summary + if (selectedTask?.taskType === "TASK_SUMMARY") + return ; + + return !selectedTask ? null : ( + + {errorMessage && ( + + )} + + + + + + + + + theme.palette?.mode === "dark" ? colors.black : colors.white, + }} + > + + + + {selectedTask.workflowTask.name} + + + + {selectedTask?.status === "PENDING" ? null : ( + + + + {selectedTask.taskId} + + + + )} + + {retryIterationOptions && retryIterationOptions?.length > 1 ? ( + + + { + return { + label: ( + + ), + handler: () => handleSelectTask(option), + }; + }) ?? [] + } + > + + + + + ) : null} + {selectedTask?.taskType === TaskType.DO_WHILE && ( + + )} + + {((selectedTask?.workflowTask?.type !== TaskType.DO_WHILE && + selectedTask?.workflowTask?.type !== TaskType.FORK_JOIN) || + rerunFromForkAndDowhileTasksEnabled) && ( + {maybeRerunTask} + )} + + + 0 ? ( + ({ + label: ( + <> + {dropdownIcon(option.status)}{" "} + {option?.workflowTask?.taskReferenceName} + + ), + handler: () => handleSelectTask(option), + }))} + > + Instances + + ) : null + } + containerQueryState={containerQueryState} + /> + + + + + + + changeCurrentTab(SUMMARY_TAB)} /> + changeCurrentTab(INPUT_TAB)} + disabled={!selectedTask.status} + /> + changeCurrentTab(OUTPUT_TAB)} + disabled={!selectedTask.status} + /> + changeCurrentTab(LOGS_TAB)} + disabled={!selectedTask.status} + /> + changeCurrentTab(JSON_TAB)} + disabled={!selectedTask.status} + /> + changeCurrentTab(DEFINITION_TAB)} + /> + + + {currentTab === SUMMARY_TAB && ( + + + {maybeStatusForm} + + )} + {currentTab === INPUT_TAB && ( + + )} + {currentTab === OUTPUT_TAB && ( + + )} + {currentTab === LOGS_TAB && ( + + + + )} + {currentTab === JSON_TAB && ( + + )} + {currentTab === DEFINITION_TAB && ( + + )} + + + ); +}; + +function dropdownIcon(status: string) { + let icon; + switch (status) { + case TaskStatus.COMPLETED: + icon = "\u2705"; + break; // Green-checkmark + case TaskStatus.COMPLETED_WITH_ERRORS: + icon = "\u2757"; + break; // Exclamation + case TaskStatus.CANCELED: + icon = "\uD83D\uDED1"; + break; // stopsign + case TaskStatus.IN_PROGRESS: + case TaskStatus.SCHEDULED: + icon = "\u231B"; + break; // hourglass + case TaskStatus.TIMED_OUT: + icon = "\u26D4"; + break; + case TaskStatus.FAILED: + icon = "\u2757"; + break; + default: + icon = "\u274C"; // red-X + } + + return icon + "\u2003"; +} diff --git a/ui-next/src/pages/execution/RightPanel/SummaryTask.tsx b/ui-next/src/pages/execution/RightPanel/SummaryTask.tsx new file mode 100644 index 0000000000..4c7a6e5bd0 --- /dev/null +++ b/ui-next/src/pages/execution/RightPanel/SummaryTask.tsx @@ -0,0 +1,74 @@ +import { FunctionComponent } from "react"; +import { Grid, Paper } from "@mui/material"; +import { X as CloseIcon } from "@phosphor-icons/react"; +import { ExecutionTask, TaskStatus } from "types"; +import { taskStatusCompareFn } from "utils"; + +import { KeyValueTable } from "components"; +import StatusBadge from "components/StatusBadge"; +import { useLocation } from "react-router"; +import { usePushHistory } from "utils/hooks/usePushHistory"; +import IconButton from "components/MuiIconButton"; +import MuiTypography from "components/MuiTypography"; + +interface TaskSummaryProps { + selectedTask: ExecutionTask; + onClose: () => void; +} + +export const SummaryTask: FunctionComponent = ({ + selectedTask, + onClose, +}) => { + const location = useLocation(); + const pushHistory = usePushHistory(); + + return ( + + + + + + + + There are way too much tasks to render here is a summary of the + nested tasks.{" "} + { + pushHistory(`${location.pathname}?tab=taskList`); + onClose(); + }} + > + Click here + {" "} + to see the list of tasks. + + + + , + ) + + .sort(([key1], [key2]) => taskStatusCompareFn(key1, key2)) + .map(([key, value]: [string, string]) => ({ + label: , + value, + }))} + /> + + + + ); +}; diff --git a/ui-next/src/pages/execution/RightPanel/index.ts b/ui-next/src/pages/execution/RightPanel/index.ts new file mode 100644 index 0000000000..1890c11a77 --- /dev/null +++ b/ui-next/src/pages/execution/RightPanel/index.ts @@ -0,0 +1,2 @@ +export * from "./RightPanel"; +export * from "./state"; diff --git a/ui-next/src/pages/execution/RightPanel/state/actions.ts b/ui-next/src/pages/execution/RightPanel/state/actions.ts new file mode 100644 index 0000000000..fb85cb23be --- /dev/null +++ b/ui-next/src/pages/execution/RightPanel/state/actions.ts @@ -0,0 +1,108 @@ +import { assign, sendParent, DoneInvokeEvent, DoneEvent } from "xstate"; +import { + ChangeCurrentTabEvent, + ClearErrorMessageEvent, + RightPanelContext, + SetSelectedTaskEvent, + SetUpdatedExecutionEvent, + UpdateTaskLogsEvent, +} from "./types"; +import { ExecutionTask } from "types"; +import { ExecutionActionTypes } from "../../state/types"; +import { taskWithLatestIteration } from "pages/execution/helpers"; + +export const persistTaskDetails = assign< + RightPanelContext, + DoneInvokeEvent +>({ + taskDetails: (_, { data }) => data, +}); + +export const persistSelectedTask = assign< + RightPanelContext, + SetSelectedTaskEvent +>({ + selectedTask: (_, { selectedTask }) => selectedTask, +}); + +export const notifyTaskUpdateToParent = sendParent( + ExecutionActionTypes.REFETCH, +); + +export const notifySelectedTaskUpdateToParent = sendParent< + RightPanelContext, + SetSelectedTaskEvent +>((ctx, _event) => { + return { + type: ExecutionActionTypes.UPDATE_TASKID_IN_URL, + selectedTask: ctx.selectedTask, + }; +}); + +export const sendDoWhileIterationToParent = sendParent< + RightPanelContext, + DoneEvent +>((_ctx, { data }) => { + return { + type: ExecutionActionTypes.SET_DO_WHILE_ITERATION, + data: data, + }; +}); + +export const sendSelectedTaskToParent = sendParent< + RightPanelContext, + SetSelectedTaskEvent +>((_ctx, { selectedTask }) => { + return { + type: ExecutionActionTypes.UPDATE_SELECTED_TASK, + selectedTask: selectedTask, + }; +}); + +export const updateTaskLogs = assign( + (__context: any, event) => { + return { + taskLogs: event?.data, + }; + }, +); +export const persistError = assign< + RightPanelContext, + DoneInvokeEvent<{ + errorDetails: any; + message: string; + }> +>({ + error: (_context, { data }) => data.errorDetails?.message, +}); + +export const clearErrorMessage = assign< + RightPanelContext, + ClearErrorMessageEvent +>(() => { + return { + error: undefined, + }; +}); + +export const updateCurrentTab = assign< + RightPanelContext, + ChangeCurrentTabEvent +>({ + currentTab: (__, { currentTab }) => currentTab, +}); + +export const extractUpdates = assign< + RightPanelContext, + SetUpdatedExecutionEvent +>((context, { execution, executionStatusMap }) => { + return { + executionStatusMap, + selectedTask: + taskWithLatestIteration( + execution.tasks, + context.selectedTask?.referenceTaskName, + context.selectedTask?.taskId, + ) || context.selectedTask, + }; +}); diff --git a/ui-next/src/pages/execution/RightPanel/state/guards.ts b/ui-next/src/pages/execution/RightPanel/state/guards.ts new file mode 100644 index 0000000000..a8fd54b210 --- /dev/null +++ b/ui-next/src/pages/execution/RightPanel/state/guards.ts @@ -0,0 +1,33 @@ +import * as TabsList from "pages/execution/state/constants"; +import { RightPanelContext } from "./types"; +import isNil from "lodash/isNil"; + +const UPDATABLE_STATUSES = ["IN_PROGRESS", "SCHEDULED"]; + +export const isSelectedTaskStatusUpdatable = ({ + selectedTask, + taskDetails, +}: RightPanelContext) => { + if (selectedTask != null) { + const { status } = !isNil(selectedTask?.status) + ? selectedTask + : taskDetails!; + + return UPDATABLE_STATUSES.includes(status); + } +}; + +export const isSummaryTab = ({ currentTab }: RightPanelContext) => + currentTab === TabsList.SUMMARY_TAB; + +export const isInputTab = ({ currentTab }: RightPanelContext) => + currentTab === TabsList.INPUT_TAB; + +export const isOutputTab = ({ currentTab }: RightPanelContext) => + currentTab === TabsList.OUTPUT_TAB; + +export const isLogsTab = ({ currentTab }: RightPanelContext) => + currentTab === TabsList.LOGS_TAB; + +export const isJsonTab = ({ currentTab }: RightPanelContext) => + currentTab === TabsList.JSON_TAB; diff --git a/ui-next/src/pages/execution/RightPanel/state/hook.ts b/ui-next/src/pages/execution/RightPanel/state/hook.ts new file mode 100644 index 0000000000..d5474d5ace --- /dev/null +++ b/ui-next/src/pages/execution/RightPanel/state/hook.ts @@ -0,0 +1,131 @@ +import { useSelector } from "@xstate/react"; +import { useMemo } from "react"; +import { useQueryState } from "react-router-use-location-state"; +import { DoWhileSelection, ExecutionTask } from "types/Execution"; +import { ActorRef, State } from "xstate"; +import { + RightPanelContext, + RightPanelContextEventTypes, + RightPanelEvents, + SetSelectedTaskEvent, +} from "./types"; +import { TaskStatus } from "types/TaskStatus"; + +export const useRightPanelActor = ( + rightPanelActor: ActorRef, +) => { + const send = rightPanelActor.send; + const selectedTask = useSelector( + rightPanelActor, + (state: State) => { + const selectedTask = state.context.selectedTask; + return selectedTask; + }, + ); + + const [_taskId, handleTaskId] = useQueryState("taskId", ""); + const executionStatusMap = useSelector( + rightPanelActor, + (state: State) => state.context.executionStatusMap, + ); + + const selectedTaskInStatusMap = useMemo(() => { + if (selectedTask != null && executionStatusMap != null) { + const maybeTask = + executionStatusMap[selectedTask.workflowTask.taskReferenceName]; + return maybeTask; + } + }, [executionStatusMap, selectedTask]); + + const retryIterationOptions = + selectedTaskInStatusMap?.loopOver && + [...(selectedTaskInStatusMap?.loopOver ?? [])].reverse(); + const maybeSiblings = selectedTaskInStatusMap?.related?.siblings || []; + + const isSelectedTaskInProgressStatus = useSelector( + rightPanelActor, + (state: State) => + state.context?.selectedTask?.status && + [ + TaskStatus.PENDING, + TaskStatus.SCHEDULED, + TaskStatus.IN_PROGRESS, + ].includes(state?.context?.selectedTask?.status), + ); + // this condition check is required as there is a backend bug which returns the previous iterations outputdata when rerunning from a task in progress status + const isSelectedTaskIsARetry = useSelector( + rightPanelActor, + (state: State) => + state.context?.selectedTask?.retryCount && + state.context?.selectedTask?.retryCount > 0, + ); + + return [ + { + selectedTask, + retryIterationOptions, + maybeSiblings, + isIteration: useSelector( + rightPanelActor, + (state: State) => + state.context.selectedTask?.loopOverTask, + ), + errorMessage: useSelector( + rightPanelActor, + (state: State) => state.context.error, + ), + taskLogs: useSelector( + rightPanelActor, + (state: State) => state?.context?.taskLogs, + ), + currentTab: useSelector( + rightPanelActor, + (state: State) => state?.context?.currentTab, + ), + isReRunFromTaskInProgress: + isSelectedTaskInProgressStatus && isSelectedTaskIsARetry, + }, + { + handleClosePanel: () => { + send({ + type: RightPanelContextEventTypes.CLOSE_RIGHT_PANEL, + }); + }, + handleChangeTaskStatus: (status: string, body: string) => { + send({ + type: RightPanelContextEventTypes.UPDATE_SELECTED_TASK_STATUS, + payload: { + status, + body, + }, + }); + }, + handleReRunRequest: () => { + send({ + type: RightPanelContextEventTypes.RE_RUN_WORKFLOW_FROM_TASK, + }); + }, + clearErrorMessage: () => { + send({ + type: RightPanelContextEventTypes.CLEAR_ERROR_MESSAGE, + }); + }, + handleSelectTask: (selectedTask: ExecutionTask) => { + const selectedTaskEvent: SetSelectedTaskEvent = { + type: RightPanelContextEventTypes.SET_SELECTED_TASK, + selectedTask: selectedTask, + }; + if (selectedTask?.taskId) { + handleTaskId(selectedTask?.taskId); + } + send(selectedTaskEvent); + }, + handleSelectDoWhileIteration: (data: DoWhileSelection) => { + send({ + type: RightPanelContextEventTypes.SET_DO_WHILE_ITERATION, + data: data, + }); + }, + }, + ] as const; +}; diff --git a/ui-next/src/pages/execution/RightPanel/state/index.ts b/ui-next/src/pages/execution/RightPanel/state/index.ts new file mode 100644 index 0000000000..79fd82215f --- /dev/null +++ b/ui-next/src/pages/execution/RightPanel/state/index.ts @@ -0,0 +1,2 @@ +export * from "./machine"; +export * from "./types"; diff --git a/ui-next/src/pages/execution/RightPanel/state/machine.ts b/ui-next/src/pages/execution/RightPanel/state/machine.ts new file mode 100644 index 0000000000..53e79f1659 --- /dev/null +++ b/ui-next/src/pages/execution/RightPanel/state/machine.ts @@ -0,0 +1,196 @@ +import { createMachine } from "xstate"; +import { + RightPanelContext, + RightPanelContextEventTypes, + RightPanelStates, + RightPanelEvents, +} from "./types"; + +import * as services from "./services"; +import * as actions from "./actions"; +import * as guards from "./guards"; +import { ExecutionActionTypes } from "pages/execution/state/types"; +import { SUMMARY_TAB } from "pages/execution/state/constants"; + +export const rightPanelMachine = createMachine< + RightPanelContext, + RightPanelEvents +>( + { + id: "rightPanelMachine", + predictableActionArguments: true, + initial: "init", + context: { + selectedTask: undefined, + authHeaders: undefined, + taskLogs: undefined, + currentTab: SUMMARY_TAB, + executionStatusMap: undefined, + }, + states: { + init: { + type: "parallel", + states: { + main: { + initial: RightPanelStates.IDLE, + states: { + [RightPanelStates.IDLE]: { + on: { + [RightPanelContextEventTypes.SET_SELECTED_TASK]: { + actions: [ + "persistSelectedTask", + "sendSelectedTaskToParent", + ], + }, + [RightPanelContextEventTypes.CLOSE_RIGHT_PANEL]: { + target: RightPanelStates.END, + }, + [RightPanelContextEventTypes.SET_UPDATED_EXECUTION]: { + actions: [ + "extractUpdates", + "notifySelectedTaskUpdateToParent", + ], + }, + [RightPanelContextEventTypes.RE_RUN_WORKFLOW_FROM_TASK]: { + target: RightPanelStates.RERUN_WORKFLOW_FROM_TASK, + }, + [RightPanelContextEventTypes.CLEAR_ERROR_MESSAGE]: { + actions: ["clearErrorMessage"], + }, + [RightPanelContextEventTypes.SET_DO_WHILE_ITERATION]: { + actions: ["sendDoWhileIterationToParent"], + }, + }, + }, + + [RightPanelStates.RERUN_WORKFLOW_FROM_TASK]: { + invoke: { + id: "reRunWoflowFromTaskService", + src: "reRunWoflowFromTask", + onDone: { + actions: ["notifyTaskUpdateToParent"], + target: RightPanelStates.IDLE, + }, + onError: { + actions: ["persistError"], + target: RightPanelStates.IDLE, + }, + }, + }, + [RightPanelStates.END]: { + always: { + target: "#rightPanelMachine.final", + }, + }, + }, + }, + [RightPanelStates.DETAILED_SECTION]: { + initial: RightPanelStates.SUMMARY, + on: { + [RightPanelContextEventTypes.CHANGE_CURRENT_TAB]: { + target: ".addressChangeTab", + actions: ["updateCurrentTab"], + }, + }, + states: { + addressChangeTab: { + always: [ + { + target: RightPanelStates.SUMMARY, + cond: "isSummaryTab", + }, + { + target: RightPanelStates.INPUT, + cond: "isInputTab", + }, + { + target: RightPanelStates.OUTPUT, + cond: "isOutputTab", + }, + { + target: RightPanelStates.LOGS, + cond: "isLogsTab", + }, + { + target: RightPanelStates.JSON, + cond: "isJsonTab", + }, + { target: RightPanelStates.DEFINITION }, + ], + }, + [RightPanelStates.SUMMARY]: { + initial: "summaryIdle", + on: { + [RightPanelContextEventTypes.UPDATE_SELECTED_TASK_STATUS]: { + target: `.${RightPanelStates.UPDATE_TASK_STATUS}`, + cond: "isSelectedTaskStatusUpdatable", + }, + }, + states: { + summaryIdle: {}, + [RightPanelStates.UPDATE_TASK_STATUS]: { + invoke: { + id: "changeTaskStatus", + src: "updateTaskState", + onDone: { + actions: ["notifyTaskUpdateToParent"], + target: "summaryIdle", + }, + onError: { + actions: ["persistError"], + target: "summaryIdle", + }, + }, + }, + }, + }, + [RightPanelStates.INPUT]: {}, + [RightPanelStates.OUTPUT]: {}, + [RightPanelStates.LOGS]: { + initial: RightPanelStates.FETCH_SELECTED_TASK_LOGS, + on: { + [ExecutionActionTypes.FETCH_FOR_LOGS]: { + target: `.${RightPanelStates.FETCH_SELECTED_TASK_LOGS}`, + }, + [RightPanelContextEventTypes.SET_SELECTED_TASK]: { + target: `.${RightPanelStates.FETCH_AFTER}`, + }, + }, + states: { + logsIdle: {}, + [RightPanelStates.FETCH_SELECTED_TASK_LOGS]: { + invoke: { + id: "getTaskLogs", + src: "fetchTaskLogs", + onDone: { + actions: ["updateTaskLogs"], + target: "logsIdle", + }, + }, + }, + [RightPanelStates.FETCH_AFTER]: { + after: { + 300: { + target: RightPanelStates.FETCH_SELECTED_TASK_LOGS, + }, + }, + }, + }, + }, + [RightPanelStates.JSON]: {}, + [RightPanelStates.DEFINITION]: {}, + }, + }, + }, + }, + final: { + type: "final", + }, + }, + }, + { + services, + actions: actions as any, + guards: guards as any, + }, +); diff --git a/ui-next/src/pages/execution/RightPanel/state/services.ts b/ui-next/src/pages/execution/RightPanel/state/services.ts new file mode 100644 index 0000000000..4b3cddeee9 --- /dev/null +++ b/ui-next/src/pages/execution/RightPanel/state/services.ts @@ -0,0 +1,110 @@ +import { RightPanelContext, UpdateSelectedTaskStatus } from "./types"; +import { fetchWithContext } from "plugins/fetch"; +import { getErrors } from "utils"; + +// This was rolled back since it does not work for COMPLETED workflows + +/* export const fetchForTaskDetailsService = async ({ */ +/* taskId, */ +/* authHeaders: headers, */ +/* }: RightPanelContext) => { */ +/* const url = `tasks/${taskId}`; */ +/* const result = await queryClient.fetchQuery([fetchContext.stack, url], () => */ +/* fetchWithContext(url, fetchContext, { headers }) */ +/* ); */ +/* return result; */ +/* }; */ + +export const updateTaskState = async ( + { executionId, selectedTask, authHeaders }: RightPanelContext, + event: any, +) => { + const { + payload: { status, body = {} }, + } = event as UpdateSelectedTaskStatus; + const { referenceTaskName } = selectedTask!; + const url = `/tasks/${executionId}/${referenceTaskName}/${status}?workerid=conductor-ui`; + try { + const result = await fetchWithContext( + url, + {}, + { + method: "POST", + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + + body, + }, + true, + ); + return result; + } catch (error) { + const errorDetails = await getErrors(error as Response); + return Promise.reject({ originalError: error, errorDetails }); + } +}; + +export const fetchTaskLogs = async ({ + authHeaders, + selectedTask, +}: RightPanelContext) => { + if (selectedTask?.taskId === undefined) { + return Promise.reject({ + originalError: "No Selected Task", + errorDetails: { message: "No Selected Task" }, + }); + } + const path = `/tasks/${selectedTask?.taskId}/log`; + try { + const result = await fetchWithContext( + path, + {}, + { + method: "GET", + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + }, + ); + return result; + } catch (error) { + return Promise.reject(error); + } +}; +export const reRunWoflowFromTask = async ({ + authHeaders, + executionId, + selectedTask, +}: RightPanelContext) => { + if (!selectedTask) { + return Promise.reject({ + originalError: "No Selected Task", + errorDetails: { message: "No Selected Task" }, + }); + } + const url = `/workflow/${executionId}/rerun`; + try { + const result = await fetchWithContext( + url, + {}, + { + method: "POST", + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + body: JSON.stringify({ + reRunFromTaskId: selectedTask.taskId, + }), + }, + true, + ); + return result; + } catch (error) { + const errorDetails = await getErrors(error as Response); + return Promise.reject({ originalError: error, errorDetails }); + } +}; diff --git a/ui-next/src/pages/execution/RightPanel/state/types.ts b/ui-next/src/pages/execution/RightPanel/state/types.ts new file mode 100644 index 0000000000..43cccdb422 --- /dev/null +++ b/ui-next/src/pages/execution/RightPanel/state/types.ts @@ -0,0 +1,111 @@ +import { DoneInvokeEvent } from "xstate"; +import { + ExecutionTask, + AuthHeaders, + TaskLog, + WorkflowExecution, + DoWhileSelection, +} from "types"; +import { StatusMap } from "pages/execution/state/StatusMapTypes"; + +export enum RightPanelStates { + IDLE = "IDLE", + UPDATE_TASK_STATUS = "UPDATE_TASK_STATUS", + FETCH_SELECTED_TASK_LOGS = "FETCH_SELECTED_TASK_LOGS", + FETCH_AFTER = "FETCH_AFTER", + RERUN_WORKFLOW_FROM_TASK = "RERUN_WORKFLOW_FROM_TASK", + DETAILED_SECTION = "DETAILED_SECTION", + END = "END", + SUMMARY = "SUMMARY", + INPUT = "INPUT", + OUTPUT = "OUTPUT", + LOGS = "LOGS", + JSON = "JSON", + DEFINITION = "DEFINITION", +} + +export enum RightPanelContextEventTypes { + SET_SELECTED_TASK = "SET_SELECTED_TASK", + CLOSE_RIGHT_PANEL = "CLOSE_RIGHT_PANEL", + UPDATE_SELECTED_TASK_STATUS = "UPDATE_SELECTED_TASK_STATUS", + SET_UPDATED_EXECUTION = "SET_UPDATED_EXECUTION", + FETCH_FOR_LOGS = "FETCH_FOR_LOGS", + RE_RUN_WORKFLOW_FROM_TASK = "RE_RUN_WORKFLOW_FROM_TASK", + CLEAR_ERROR_MESSAGE = "CLEAR_ERROR_MESSAGE", + CHANGE_CURRENT_TAB = "CHANGE_CURRENT_TAB", + SET_DO_WHILE_ITERATION = "SET_DO_WHILE_ITERATION", +} + +export type UpdateSelectedTaskStatus = { + type: RightPanelContextEventTypes.UPDATE_SELECTED_TASK_STATUS; + payload: { + status: string; + body: string; + }; +}; + +export type ReRunWorkflowFromTaskEvent = { + type: RightPanelContextEventTypes.RE_RUN_WORKFLOW_FROM_TASK; +}; + +export type SelectedTaskType = ExecutionTask & { + selectedIteration?: ExecutionTask; + iteration?: number; +}; + +export interface RightPanelContext { + selectedTask?: ExecutionTask; + executionStatusMap?: StatusMap; + taskDetails?: ExecutionTask; + authHeaders?: AuthHeaders; + executionId?: string; + taskLogs?: TaskLog[]; + error?: string; + currentTab: number; +} + +export type SetSelectedTaskEvent = { + type: RightPanelContextEventTypes.SET_SELECTED_TASK; + selectedTask: ExecutionTask; +}; + +export type CloseRightPanelEvent = { + type: RightPanelContextEventTypes.CLOSE_RIGHT_PANEL; +}; + +export type UpdateTaskLogsEvent = { + type: RightPanelContextEventTypes.FETCH_FOR_LOGS; + data: TaskLog[]; +}; + +export type ClearErrorMessageEvent = { + type: RightPanelContextEventTypes.CLEAR_ERROR_MESSAGE; +}; + +export type ChangeCurrentTabEvent = { + type: RightPanelContextEventTypes.CHANGE_CURRENT_TAB; + currentTab: number; +}; + +export type SetUpdatedExecutionEvent = { + type: RightPanelContextEventTypes.SET_UPDATED_EXECUTION; + execution: WorkflowExecution; + executionStatusMap: StatusMap; +}; + +export type SetDoWhileIterationEvent = { + type: RightPanelContextEventTypes.SET_DO_WHILE_ITERATION; + data: DoWhileSelection; +}; + +export type RightPanelEvents = + | UpdateSelectedTaskStatus + | CloseRightPanelEvent + | UpdateTaskLogsEvent + | DoneInvokeEvent + | ReRunWorkflowFromTaskEvent + | ClearErrorMessageEvent + | SetSelectedTaskEvent + | SetUpdatedExecutionEvent + | ChangeCurrentTabEvent + | SetDoWhileIterationEvent; diff --git a/ui-next/src/pages/execution/TaskList/StatusSelect.tsx b/ui-next/src/pages/execution/TaskList/StatusSelect.tsx new file mode 100644 index 0000000000..a4bcdb28a4 --- /dev/null +++ b/ui-next/src/pages/execution/TaskList/StatusSelect.tsx @@ -0,0 +1,84 @@ +import { Box, FormControl, MenuItem, useMediaQuery } from "@mui/material"; +import { FunctionComponent, useMemo } from "react"; + +import MuiCheckbox from "components/MuiCheckbox"; +import StatusBadge from "components/StatusBadge"; +import { ConductorSelect } from "components/v1"; +import { Entries } from "types"; +import { SelectableStatus } from "./state"; + +interface StatusSelectProps { + onSelect: (selection?: SelectableStatus[]) => void; + value: any[]; + summary?: Record; +} + +const ALL = "ALL"; +const label = "Status Filter"; + +export const StatusSelect: FunctionComponent = ({ + onSelect, + value, + summary = {}, +}) => { + const isSmallWidth = useMediaQuery((theme: any) => + theme.breakpoints.down("sm"), + ); + + const options = useMemo( + () => + (Object.entries(summary) as Entries>) + .map( + ([statusId, amount]): { + statusId: SelectableStatus; + amount: number; + } => ({ + statusId, + amount, + }), + ) + .sort((a, b) => a.statusId.localeCompare(b.statusId)), + [summary], + ); + + const handleSelection = (event: any) => { + const eventValue = event.target.value; + onSelect(eventValue === ALL ? undefined : eventValue); + }; + + return ( + + + + Array.isArray(selected) && selected.length > 0 + ? selected.join(", ") + : ALL, + }} + sx={{ minWidth: "160px" }} + > + {options.map(({ statusId, amount }) => ( + + item === statusId) >= 0} + /> + + + ))} + + + + ); +}; diff --git a/ui-next/src/pages/execution/TaskList/TaskList.tsx b/ui-next/src/pages/execution/TaskList/TaskList.tsx new file mode 100644 index 0000000000..fcd508c5a4 --- /dev/null +++ b/ui-next/src/pages/execution/TaskList/TaskList.tsx @@ -0,0 +1,231 @@ +import { Box, Stack } from "@mui/material"; +import { DataTable } from "components"; +import { ColumnCustomType, LegacyColumn } from "components/DataTable/types"; +import StatusBadge from "components/StatusBadge"; +import { FunctionComponent, useContext } from "react"; +import { ColorModeContext } from "theme/material/ColorModeContext"; +import { colors } from "theme/tokens/variables"; +import { ExecutionTask } from "types"; +import { calculateDifferentTime } from "utils/utils"; +import { ActorRef } from "xstate"; +import { + SelectableStatus, + TaskListMachineEvents, + useTaskListActor, +} from "./state"; +import { StatusSelect } from "./StatusSelect"; +import { clickHandler, taskIdRenderer } from "pages/execution/componentHelpers"; + +const calculateExecutionTime = (startTime: number, endTime: number) => + new Date(endTime).getTime() - new Date(startTime).getTime(); + +const customSortForExecutionTime = ( + rowA: ExecutionTask, + rowB: ExecutionTask, +) => { + const executionTimeA = + rowA.startTime && rowA.endTime + ? calculateExecutionTime(rowA.startTime, rowA.endTime) + : 0; + const executionTimeB = + rowB.startTime && rowB.endTime + ? calculateExecutionTime(rowB.startTime, rowB.endTime) + : 0; + + return executionTimeA - executionTimeB; +}; + +export const MIN_DATE_WIDTH = "175px"; + +interface TaskListProps { + taskListActor: ActorRef; + executionAlert: string; +} + +export const TaskList: FunctionComponent = ({ + taskListActor, +}) => { + const [ + { taskListPage, statusFilter, totalHits, isFetching, rowsPerPage, summary }, + { + handleChangeStatus, + handleChangePage, + handleChangeRowsPerPage, + handleSelectTask, + }, + ] = useTaskListActor(taskListActor); + + const { mode } = useContext(ColorModeContext); + // const [{ expandDynamic }, { execution }] = useExecutionMachine(); + const taskDetailFields = [ + { + id: "seq", + name: "seq", + label: "Seq.", + minWidth: "50px", + maxWidth: "100px", + style: { whiteSpace: "nowrap", wordBreak: "keep-all" }, + tooltip: "The sequence number of the task", + }, + { + id: "taskId", + name: "taskId", + label: "Task Id", + minWidth: "130px", + maxWidth: "130px", + renderer: taskIdRenderer(clickHandler(handleSelectTask)), + tooltip: "The unique identifier of the task", + }, + { + id: "taskName", + name: "workflowTask.name", + label: "Task Name", + tooltip: "The name of the task", + }, + { + id: "referenceTaskName", + name: "referenceTaskName", + label: "Ref", + minWidth: "250px", + maxWidth: "350px", + tooltip: "The Reference Task Name", + }, + { + id: "taskType", + name: "workflowTask.type", + label: "Type", + minWidth: "100px", + maxWidth: "200px", + tooltip: "The Task type", + }, + { + id: "scheduledTime", + name: "scheduledTime", + type: ColumnCustomType.DATE, + label: "Scheduled Time", + minWidth: MIN_DATE_WIDTH, + maxWidth: MIN_DATE_WIDTH, + tooltip: "The time the task was scheduled to run", + }, + { + id: "startTime", + name: "startTime", + type: ColumnCustomType.DATE, + label: "Start Time", + minWidth: MIN_DATE_WIDTH, + maxWidth: MIN_DATE_WIDTH, + tooltip: "The time the task started running", + }, + { + id: "endTime", + name: "endTime", + type: ColumnCustomType.DATE, + label: "End Time", + minWidth: MIN_DATE_WIDTH, + maxWidth: MIN_DATE_WIDTH, + tooltip: "The time the task ended running", + }, + { + id: "executionTime", + name: "executionTime", + label: "Execution Time", + minWidth: "100px", + maxWidth: "200px", + renderer: (_: unknown, { startTime, endTime }: ExecutionTask) => + startTime && endTime ? calculateDifferentTime(startTime, endTime) : "", + sortFunction: customSortForExecutionTime, + tooltip: "The time the task took to run", + }, + { + id: "status", + name: "status", + grow: 0.5, + label: "Status", + minWidth: "120px", + maxWidth: "150px", + renderer: (status: SelectableStatus) => , + tooltip: "The status of the task", + }, + { + id: "updateTime", + name: "updateTime", + type: ColumnCustomType.DATE, + label: "Update Time", + tooltip: "The time the task was last updated", + }, + { + id: "callbackAfterSeconds", + name: "callbackAfterSeconds", + label: "Callback", + tooltip: "The time the task should callback", + }, + { + id: "pollCount", + name: "pollCount", + label: "Poll Count", + tooltip: "The number of times the task has been polled", + }, + ]; + + return ( + + + , + ]} + columns={taskDetailFields as LegacyColumn[]} + progressPending={isFetching} + onChangePage={(page: number) => handleChangePage!(page)} + onChangeRowsPerPage={(newPerPage: number, _page: number) => + handleChangeRowsPerPage!(newPerPage) + } + defaultShowColumns={[ + "seq", + "taskId", + "referenceTaskName", + "taskType", + "startTime", + "endTime", + "executionTime", + "scheduledTime", + "status", + ]} + pagination + hideSearch + paginationServer + paginationPerPage={rowsPerPage} + paginationRowsPerPageOptions={[20, 50, 100]} + paginationTotalRows={totalHits} + localStorageKey="taskListTable" + sortByDefault={false} + /> + + + ); +}; diff --git a/ui-next/src/pages/execution/TaskList/index.ts b/ui-next/src/pages/execution/TaskList/index.ts new file mode 100644 index 0000000000..0b50570658 --- /dev/null +++ b/ui-next/src/pages/execution/TaskList/index.ts @@ -0,0 +1,2 @@ +export * from "./TaskList"; +export * from "./state"; diff --git a/ui-next/src/pages/execution/TaskList/state/actions.ts b/ui-next/src/pages/execution/TaskList/state/actions.ts new file mode 100644 index 0000000000..b51ebd9a03 --- /dev/null +++ b/ui-next/src/pages/execution/TaskList/state/actions.ts @@ -0,0 +1,46 @@ +import { assign, DoneInvokeEvent } from "xstate"; +import { + TaskListMachineContext, + StatusFilterChangeEvent, + TaskListPageResponse, + NextPageEvent, + ChangeRowsPerPageEvent, + SendSelectionToParentEvent, +} from "./types"; +import { RightPanelContextEventTypes } from "pages/execution/RightPanel"; +import { sendParent } from "xstate/lib/actions"; + +export const persistTaskListPage = assign< + TaskListMachineContext, + DoneInvokeEvent +>({ + taskList: (_context: TaskListMachineContext, { data }) => data.results, + totalHits: (_context: TaskListMachineContext, { data }) => data.totalHits, + summary: (_context: TaskListMachineContext, { data }) => data.summary, +}); + +export const persistFilterStatus = assign< + TaskListMachineContext, + StatusFilterChangeEvent +>({ + filterStatus: (_context, { status }) => status, +}); + +export const persistNextPage = assign({ + startIndex: ({ rowsPerPage = 15 }, { page }) => (page - 1) * rowsPerPage, +}); + +export const persistRowPerPage = assign< + TaskListMachineContext, + ChangeRowsPerPageEvent +>({ + rowsPerPage: (__, { rowsPerPage }) => rowsPerPage, +}); + +export const selectTask = sendParent< + TaskListMachineContext, + SendSelectionToParentEvent +>((__, { selectedTask }) => ({ + type: RightPanelContextEventTypes.SET_SELECTED_TASK, + selectedTask, +})); diff --git a/ui-next/src/pages/execution/TaskList/state/hook.ts b/ui-next/src/pages/execution/TaskList/state/hook.ts new file mode 100644 index 0000000000..50313cd53d --- /dev/null +++ b/ui-next/src/pages/execution/TaskList/state/hook.ts @@ -0,0 +1,62 @@ +import { useActor, useSelector } from "@xstate/react"; +import { ActorRef } from "xstate"; +import { + SelectableStatus, + TaskListMachineEvents, + TaskListMachineEventTypes, +} from "./types"; +import { ExecutionTask } from "types"; + +export const useTaskListActor = ( + taskListActor: ActorRef, +) => { + const [, send] = useActor(taskListActor); + + return [ + { + taskListPage: useSelector( + taskListActor, + (state) => state.context.taskList, + ), + statusFilter: useSelector( + taskListActor, + (state) => state.context.filterStatus, + ), + totalHits: useSelector(taskListActor, (state) => state.context.totalHits), + isFetching: useSelector(taskListActor, (state) => + state.matches("fetchForTasks"), + ), + rowsPerPage: useSelector( + taskListActor, + (state) => state.context.rowsPerPage, + ), + summary: useSelector(taskListActor, (state) => state.context.summary), + }, + { + handleChangeStatus: (status?: SelectableStatus[]) => { + send({ + type: TaskListMachineEventTypes.SET_STATUS_FILTER, + status, + }); + }, + handleChangePage: (page: number) => { + send({ + type: TaskListMachineEventTypes.NEXT_PAGE, + page, + }); + }, + handleChangeRowsPerPage: (rowsPerPage: number) => { + send({ + type: TaskListMachineEventTypes.CHANGE_ROWS_PER_PAGE, + rowsPerPage, + }); + }, + handleSelectTask: (selectedTask: ExecutionTask) => { + send({ + type: TaskListMachineEventTypes.SEND_SELECTION_TO_PARENT, + selectedTask, + }); + }, + }, + ]; +}; diff --git a/ui-next/src/pages/execution/TaskList/state/index.ts b/ui-next/src/pages/execution/TaskList/state/index.ts new file mode 100644 index 0000000000..738f5d47e2 --- /dev/null +++ b/ui-next/src/pages/execution/TaskList/state/index.ts @@ -0,0 +1,3 @@ +export * from "./machine"; +export * from "./hook"; +export * from "./types"; diff --git a/ui-next/src/pages/execution/TaskList/state/machine.ts b/ui-next/src/pages/execution/TaskList/state/machine.ts new file mode 100644 index 0000000000..3ac57f0348 --- /dev/null +++ b/ui-next/src/pages/execution/TaskList/state/machine.ts @@ -0,0 +1,56 @@ +import { createMachine } from "xstate"; +import { TaskListMachineContext, TaskListMachineEventTypes } from "./types"; +import * as services from "./services"; +import * as actions from "./actions"; + +export const taskListMachine = () => + createMachine( + { + id: "taskListMachine", + predictableActionArguments: true, + initial: "fetchForTasks", + context: { + taskList: [], + startIndex: 0, + rowsPerPage: 15, + executionId: undefined, + authHeaders: undefined, + filterStatus: undefined, + totalHits: undefined, + }, + states: { + fetchForTasks: { + invoke: { + src: "fetchForTasksService", + onDone: { + target: "idle", + actions: ["persistTaskListPage"], + }, + }, + }, + idle: { + on: { + [TaskListMachineEventTypes.SET_STATUS_FILTER]: { + actions: ["persistFilterStatus"], + target: "fetchForTasks", + }, + [TaskListMachineEventTypes.NEXT_PAGE]: { + actions: ["persistNextPage"], + target: "fetchForTasks", + }, + [TaskListMachineEventTypes.CHANGE_ROWS_PER_PAGE]: { + actions: ["persistRowPerPage"], + target: "fetchForTasks", + }, + [TaskListMachineEventTypes.SEND_SELECTION_TO_PARENT]: { + actions: ["selectTask"], + }, + }, + }, + }, + }, + { + services, + actions: actions as any, + }, + ); diff --git a/ui-next/src/pages/execution/TaskList/state/services.ts b/ui-next/src/pages/execution/TaskList/state/services.ts new file mode 100644 index 0000000000..ed99884d3e --- /dev/null +++ b/ui-next/src/pages/execution/TaskList/state/services.ts @@ -0,0 +1,39 @@ +import { queryClient } from "../../../../queryClient"; +import { fetchWithContext, fetchContextNonHook } from "plugins/fetch"; +import { TaskListMachineContext } from "./types"; +import { logger } from "utils/logger"; +import { UrlOptions } from "utils/toMaybeQueryString"; +import qs from "qs"; +import _isEmpty from "lodash/isEmpty"; + +const fetchContext = fetchContextNonHook(); +const getQueryString = (content: any) => { + return _isEmpty(content) + ? "" + : `?${qs.stringify(content, { indices: false })}`; +}; + +export const fetchForTasksService = async ({ + authHeaders: headers, + executionId, + startIndex = 0, + rowsPerPage = 15, + filterStatus, +}: TaskListMachineContext) => { + const executionTasksPath = `/workflow/${executionId}/tasks${getQueryString({ + status: filterStatus, + count: rowsPerPage, + start: startIndex, + } as unknown as UrlOptions)}`; + logger.info("Will hit path to fetch for tasks ", executionTasksPath); + try { + const response = await queryClient.fetchQuery( + [fetchContext.stack, executionTasksPath], + () => fetchWithContext(executionTasksPath, fetchContext, { headers }), + ); + return response; + } catch (error) { + logger.error("Fetching task list page", error); + return Promise.reject({ message: "Error fetching task list page" }); + } +}; diff --git a/ui-next/src/pages/execution/TaskList/state/types.ts b/ui-next/src/pages/execution/TaskList/state/types.ts new file mode 100644 index 0000000000..6b63fc6909 --- /dev/null +++ b/ui-next/src/pages/execution/TaskList/state/types.ts @@ -0,0 +1,54 @@ +import { DoneInvokeEvent } from "xstate"; +import { ExecutionTask, AuthHeaders, TaskStatus } from "types"; + +export type SelectableStatus = TaskStatus; +export type TaskListMachineContext = { + taskList: ExecutionTask[]; + startIndex: number; + rowsPerPage: number; + executionId?: string; + authHeaders?: AuthHeaders; + filterStatus?: SelectableStatus[]; + summary?: Record; + totalHits?: number; +}; + +export enum TaskListMachineEventTypes { + SET_STATUS_FILTER = "SET_STATUS_FILTER", + NEXT_PAGE = "NEXT_PAGE", + CHANGE_ROWS_PER_PAGE = "CHANGE_ROWS_PER_PAGE", + SEND_SELECTION_TO_PARENT = "SEND_SELECTION_TO_PARENT", +} + +export type StatusFilterChangeEvent = { + type: TaskListMachineEventTypes.SET_STATUS_FILTER; + status?: SelectableStatus[]; +}; + +export type NextPageEvent = { + type: TaskListMachineEventTypes.NEXT_PAGE; + page: number; +}; + +export type ChangeRowsPerPageEvent = { + type: TaskListMachineEventTypes.CHANGE_ROWS_PER_PAGE; + rowsPerPage: number; +}; + +export type SendSelectionToParentEvent = { + type: TaskListMachineEventTypes.SEND_SELECTION_TO_PARENT; + selectedTask: ExecutionTask; +}; + +export type TaskListPageResponse = { + results: ExecutionTask[]; + totalHits: number; + summary: Record; +}; + +export type TaskListMachineEvents = + | DoneInvokeEvent + | SendSelectionToParentEvent + | NextPageEvent + | ChangeRowsPerPageEvent + | StatusFilterChangeEvent; diff --git a/ui-next/src/pages/execution/TaskLogs.tsx b/ui-next/src/pages/execution/TaskLogs.tsx new file mode 100644 index 0000000000..1eda84be3d --- /dev/null +++ b/ui-next/src/pages/execution/TaskLogs.tsx @@ -0,0 +1,103 @@ +import { Box } from "@mui/material"; +import { Input, Text, Typography } from "components"; +import ClipboardCopy from "components/ClipboardCopy"; +import { useMemo, useState } from "react"; +import { TaskLog } from "types"; +import { formatToDateTimeString } from "utils/date"; +import { ActorRef } from "xstate"; +import { RightPanelEvents } from "./RightPanel/state"; +import { useRightPanelActor } from "./RightPanel/state/hook"; + +export interface TaskLogsProps { + containerQueryState: any; + rightPanelActor: ActorRef; +} + +export default function TaskLogs({ + containerQueryState, + rightPanelActor, +}: TaskLogsProps) { + const [{ taskLogs }] = useRightPanelActor(rightPanelActor); + + const [filteredLogs, setFilteredLogs] = useState([]); + const [searchValue, setSearchValue] = useState(""); + + useMemo(() => { + setFilteredLogs(() => { + const tempSearchValue = searchValue.trim().toLowerCase(); + return ( + taskLogs?.reduce((result: TaskLog[], item: TaskLog) => { + const createdTimeString = formatToDateTimeString(item.createdTime); + + if ( + createdTimeString.includes(tempSearchValue) || + item.log?.toLowerCase()?.includes(tempSearchValue) + ) { + result.push({ + ...item, + createdTime: createdTimeString, + }); + } + + return result; + }, []) || [] + ); + }); + }, [searchValue, setFilteredLogs, taskLogs]); + + return ( + + {filteredLogs?.length > 0 && ( + `[${taskLog.createdTime}] ${taskLog.log}`) + .join("\n")} + sx={{ + mb: 1, + pr: 1, + }} + iconPlacement="start" + > + + Copy logs + + + )} + + + {filteredLogs?.length > 0 ? ( + + {filteredLogs.map((item: TaskLog, index) => ( + + [{item.createdTime} + ]  + {item.log} + + ))} + + ) : ( + + No logs available + + )} + + ); +} diff --git a/ui-next/src/pages/execution/TaskSummary.tsx b/ui-next/src/pages/execution/TaskSummary.tsx new file mode 100644 index 0000000000..3058396e56 --- /dev/null +++ b/ui-next/src/pages/execution/TaskSummary.tsx @@ -0,0 +1,223 @@ +import _isFinite from "lodash/isFinite"; +import _get from "lodash/get"; +import { NavLink, KeyValueTable } from "components"; +import { Link, Paper } from "@mui/material"; +import { ExecutionTask, TaskType } from "types"; +import { ReactNode, useMemo } from "react"; + +interface TaskSummaryProps { + taskResult: ExecutionTask; +} + +type DataType = { + label: string; + value: ReactNode | string | number | null; + type?: string; +}; + +export default function TaskSummary({ taskResult }: TaskSummaryProps) { + const shouldDisplayEvaluatedCase = useMemo( + () => ["DECISION", "SWITCH"].includes(taskResult.taskType), + [taskResult.taskType], + ); + + // To accommodate unexecuted tasks, read type & name & ref out of workflowTask + const data = [ + { label: "Task type", value: taskResult.workflowTask.type }, + { + label: "Status", + value: taskResult.status || "Not executed", + type: "status", + }, + { label: "Task name", value: taskResult.workflowTask.name }, + { + label: "Task reference", + value: taskResult.workflowTask.taskReferenceName, + }, + ] as DataType[]; + + if (taskResult.domain) { + data.push({ label: "Domain", value: taskResult.domain }); + } + + if (taskResult.taskId) { + data.push({ label: "Task execution id", value: taskResult.taskId }); + } + + if (taskResult.correlationId) { + data.push({ label: "Correlation id", value: taskResult.correlationId }); + } + + if (_isFinite(taskResult.retryCount)) { + data.push({ label: "Retry count", value: taskResult.retryCount }); + } + + if (taskResult.scheduledTime) { + data.push({ + label: "Scheduled time", + value: taskResult.scheduledTime > 0 && taskResult.scheduledTime, + type: "date", + }); + } + if (taskResult.startTime) { + data.push({ + label: "Start time", + value: taskResult.startTime > 0 && taskResult.startTime, + type: "date", + }); + } + if (taskResult.endTime) { + data.push({ label: "End time", value: taskResult.endTime, type: "date" }); + } + if (taskResult.startTime && taskResult.endTime) { + data.push({ + label: "Duration", + value: + taskResult.startTime > 0 && taskResult.endTime - taskResult.startTime, + type: "duration", + }); + } + if (taskResult.reasonForIncompletion) { + const statusIndex = data.findIndex((item) => item.label === "Status"); + data.splice(statusIndex + 1, 0, { + label: "Reason for incompletion", + value: taskResult.reasonForIncompletion, + type: "error", + }); + } + if (taskResult.workerId) { + data.push({ + label: "Worker", + value: taskResult.workerId, + type: "workerId", + }); + } + if (taskResult.pollCount) { + data.push({ + label: "Poll count", + value: taskResult.pollCount, + }); + } + if (taskResult.seq) { + data.push({ + label: "Sequence", + value: taskResult.seq, + }); + } + if (taskResult.queueWaitTime) { + data.push({ + label: "Queue wait time", + value: taskResult.queueWaitTime, + }); + } + if (shouldDisplayEvaluatedCase) { + const caseOutput = taskResult.outputData?.caseOutput; + data.push({ + label: "Evaluated case", + value: caseOutput ? caseOutput[0] : null, + }); + } + if (taskResult?.inputData?.integrationName) { + data.push({ + label: "Integration name", + value: ( + + {taskResult.inputData?.integrationName} + + ), + }); + } + if (taskResult.workflowTask.type === TaskType.SUB_WORKFLOW) { + data.push({ + label: "Subworkflow definition", + value: ( + + {taskResult.inputData?.subWorkflowName} + + ), + }); + if (_get(taskResult, "outputData.subWorkflowId")) { + data.push({ + label: "Subworkflow id", + value: ( + + {taskResult.outputData?.subWorkflowId} + + ), + }); + } + } + + if ( + taskResult.workflowTask.type === TaskType.START_WORKFLOW && + taskResult.outputData?.workflowId + ) { + data.push({ + label: "Start workflow", + value: ( + + {`${window.location.origin}/execution/${taskResult.outputData?.workflowId}`} + + ), + }); + } + if ( + taskResult.workflowTask.type === TaskType.DYNAMIC && + taskResult.inputData?.taskToExecute === "SUB_WORKFLOW" + ) { + const { subWorkflowName } = taskResult.inputData ?? {}; + const { subWorkflowId } = taskResult.outputData ?? {}; + + if (subWorkflowName) { + data.push({ + label: "Subworkflow definition", + value: ( + + {subWorkflowName} + + ), + }); + } + if (subWorkflowId) { + data.push({ + label: "Subworkflow id", + value: ( + + {subWorkflowId} + + ), + }); + } + } + + return ( + + + + ); +} diff --git a/ui-next/src/pages/execution/Timeline.jsx b/ui-next/src/pages/execution/Timeline.jsx new file mode 100644 index 0000000000..3707938483 --- /dev/null +++ b/ui-next/src/pages/execution/Timeline.jsx @@ -0,0 +1,314 @@ +import ExpandIcon from "@mui/icons-material/Expand"; +import { Box, Tooltip, Typography } from "@mui/material"; +import _debounce from "lodash/debounce"; +import _first from "lodash/first"; +import _flow from "lodash/flow"; +import _identity from "lodash/identity"; +import _last from "lodash/last"; +import { useEffect, useMemo, useRef, useState } from "react"; +import Timeline from "react-vis-timeline"; +import { ZoomControlsButton } from "shared/ZoomControlsButton"; +import { colors } from "theme/tokens/variables"; +import { formatDate } from "utils"; +import NoAnimRangeSlider from "./NoAnimRangeSlider"; +import { processTasksToGroupsAndItems } from "./timelineUtils"; + +import "./timeline.scss"; + +function valuetext(value) { + const valueText = formatDate(value, "dd-MM-yyyy hh:mm:ss SSS"); + return valueText; +} + +export default function TimelineComponent({ + tasks, + onClick, + selectedTask, + executionStatusMap, +}) { + const timelineRef = useRef(); + + const handleChange = (event, newValue) => { + setRangeSliderValue(newValue); + timelineRef.current.timeline.setWindow(newValue[0], newValue[1], { + animation: false, + }); + }; + + let selectedId = null; + if (selectedTask) { + selectedId = selectedTask.taskId; + } + + const [groups, items] = useMemo(() => { + return processTasksToGroupsAndItems(tasks, executionStatusMap); + }, [tasks, executionStatusMap]); + + const handleClick = (e) => { + const { group, item, what } = e; + if (group && what !== "background") { + onClick({ + ref: group, + taskId: item, + }); + } + }; + + const currentTime = new Date(); + const minDate = items.length > 0 ? _first(items)?.start : currentTime; + const lastDate = items.length > 0 ? _last(items)?.end : currentTime; + // the last item isn't necessary the latest to finish + let lastEnd = lastDate; + items.forEach((i) => { + if (i.end.getTime() > lastEnd.getTime()) { + lastEnd = i.end; + } + }); + + const diffMilli = lastEnd.getTime() - minDate.getTime(); + // Less than 100ms has odd behaviour with the Timeline component and needs more buffer + const endBuffer = diffMilli * (diffMilli < 100 ? 0.06 : 0.01); + const maxDate = new Date(lastEnd.getTime() + endBuffer); + + const onFit = () => { + timelineRef.current.timeline.fit(); + setRangeSliderValue([minDate.getTime(), maxDate.getTime()]); + }; + + const [rangeSliderValue, setRangeSliderValue] = useState([ + minDate.getTime(), + maxDate.getTime(), + ]); + + const debouncedSetRangeSliderValue = useRef( + _debounce((e) => { + if (e.byUser) { + setRangeSliderValue([e.start.getTime(), e.end.getTime()]); + } + }, 100), + [setRangeSliderValue], + ); + + if (timelineRef.current) { + timelineRef.current.timeline.off( + "rangechanged", + debouncedSetRangeSliderValue.current, + ); + timelineRef.current.timeline.on( + "rangechanged", + debouncedSetRangeSliderValue.current, + ); + } + + useEffect(() => { + if (!timelineRef.current?.timeline) return; + timelineRef.current.timeline.setItems(items); + timelineRef.current.timeline.setGroups(groups); + }, [groups, items]); + + return ( + + + {maxDate > minDate && ( + + + + + + )} + + + + theme.palette?.mode === "dark" ? colors.gray14 : undefined, + }, + }} + > + + + + + + + + + Task Queue Time + + + + + + Task Execution Duration + + + + + + + + + + Conductor Latency (Gap) + + + + + + {maxDate > minDate && ( + + Adjust visible range + "Temperature range"} + value={rangeSliderValue} + min={minDate.getTime()} + max={maxDate.getTime()} + onChange={handleChange} + valueLabelDisplay="auto" + valueLabelFormat={valuetext} + /> + + )} + + ); +} diff --git a/ui-next/src/pages/execution/Timeline.test.ts b/ui-next/src/pages/execution/Timeline.test.ts new file mode 100644 index 0000000000..2164f09a66 --- /dev/null +++ b/ui-next/src/pages/execution/Timeline.test.ts @@ -0,0 +1,537 @@ +import { ExecutionTask } from "types/Execution"; +import { processTasksToGroupsAndItems } from "./timelineUtils"; + +// Helper function to create mock tasks +const createMockTask = (overrides = {}) => ({ + taskId: "task-1", + referenceTaskName: "task_ref", + status: "COMPLETED", + startTime: Date.now() - 1000, + endTime: Date.now(), + scheduledTime: null, + workflowTask: { + name: "Task Name", + taskReferenceName: "task_ref", + type: "SIMPLE", + }, + inputData: {}, + iteration: 0, + ...overrides, +}); + +// Helper function to create mock execution status map +const createMockExecutionStatusMap = (overrides = {}) => ({ + task_ref: { + related: null, + }, + ...overrides, +}); + +describe("Timeline Groups and Items Processing", () => { + describe("Basic Task Processing (Ideal Case)", () => { + it("should create groups and items for normal tasks", () => { + const tasks = [ + createMockTask({ + taskId: "task-1", + referenceTaskName: "task1_ref", + workflowTask: { + name: "Task 1", + taskReferenceName: "task1_ref", + type: "SIMPLE", + }, + }), + createMockTask({ + taskId: "task-2", + referenceTaskName: "task2_ref", + workflowTask: { + name: "Task 2", + taskReferenceName: "task2_ref", + type: "SIMPLE", + }, + }), + ]; + + const executionStatusMap = createMockExecutionStatusMap(); + const [groups, _items] = processTasksToGroupsAndItems( + tasks as unknown as ExecutionTask[], + executionStatusMap, + ); + + expect(groups).toHaveLength(2); + expect(_items).toHaveLength(2); + expect(groups[0].id).toBe("task1_ref"); + expect(groups[1].id).toBe("task2_ref"); + expect(_items[0].id).toBe("task-1"); + expect(_items[1].id).toBe("task-2"); + }); + + it("should set treeLevel based on executionStatusMap", () => { + const tasks = [ + createMockTask({ + referenceTaskName: "task1_ref", + workflowTask: { + taskReferenceName: "task1_ref", + type: "SIMPLE", + }, + }), + ]; + + const executionStatusMap = createMockExecutionStatusMap({ + task1_ref: { + related: "some_related_task", + }, + }); + + const [groups, _items] = processTasksToGroupsAndItems( + tasks as unknown as ExecutionTask[], + executionStatusMap, + ); + + expect(groups).toHaveLength(1); + expect(groups[0].treeLevel).toBe(2); + }); + }); + + describe("FORK_JOIN_DYNAMIC - Ideal Case (Before Fix Would Work)", () => { + it("should set nestedGroups correctly when forkedTasks match group IDs exactly", () => { + const tasks = [ + createMockTask({ + taskId: "fork-task-1", + referenceTaskName: "fork_ref", + workflowTask: { + name: "Fork Task", + taskReferenceName: "fork_ref", + type: "FORK_JOIN_DYNAMIC", + }, + inputData: { + forkedTasks: ["child1", "child2"], + }, + }), + createMockTask({ + taskId: "child1-task", + referenceTaskName: "child1", + workflowTask: { + name: "Child 1", + taskReferenceName: "child1", + type: "SIMPLE", + }, + }), + createMockTask({ + taskId: "child2-task", + referenceTaskName: "child2", + workflowTask: { + name: "Child 2", + taskReferenceName: "child2", + type: "SIMPLE", + }, + }), + ]; + + const [groups, _items] = processTasksToGroupsAndItems( + tasks as unknown as ExecutionTask[], + {}, + ); + + expect(groups).toHaveLength(3); + const forkGroup = groups.find((g) => g.id === "fork_ref"); + expect(forkGroup?.nestedGroups).toEqual(["child1", "child2"]); + }); + }); + + describe("FORK_JOIN_DYNAMIC - Fix Scenario (After Fix)", () => { + it("should map forkedTasks with iteration suffix when exact match not found", () => { + const tasks = [ + createMockTask({ + taskId: "fork-task-1", + referenceTaskName: "fork_ref", + workflowTask: { + name: "Fork Task", + taskReferenceName: "fork_ref", + type: "FORK_JOIN_DYNAMIC", + }, + inputData: { + forkedTasks: ["child1", "child2"], + }, + iteration: 2, + }), + createMockTask({ + taskId: "child1-task-2", + referenceTaskName: "child1__2", + workflowTask: { + name: "Child 1", + taskReferenceName: "child1", + type: "SIMPLE", + }, + iteration: 2, + }), + createMockTask({ + taskId: "child2-task-2", + referenceTaskName: "child2__2", + workflowTask: { + name: "Child 2", + taskReferenceName: "child2", + type: "SIMPLE", + }, + iteration: 2, + }), + ]; + + const [groups, _items] = processTasksToGroupsAndItems( + tasks as unknown as ExecutionTask[], + {}, + ); + + expect(groups).toHaveLength(3); + const forkGroup = groups.find((g) => g.id === "fork_ref"); + // This is the key test - the fix should map "child1" to "child1__2" and "child2" to "child2__2" + expect(forkGroup?.nestedGroups).toEqual(["child1__2", "child2__2"]); + }); + + it("should handle multiple iterations correctly", () => { + const tasks = [ + createMockTask({ + taskId: "fork-task-1", + referenceTaskName: "fork_ref", + workflowTask: { + name: "Fork Task", + taskReferenceName: "fork_ref", + type: "FORK_JOIN_DYNAMIC", + }, + inputData: { + forkedTasks: ["child1", "child2"], + }, + iteration: 3, + }), + createMockTask({ + taskId: "child1-task-3", + referenceTaskName: "child1__3", + workflowTask: { + name: "Child 1", + taskReferenceName: "child1", + type: "SIMPLE", + }, + iteration: 3, + }), + createMockTask({ + taskId: "child2-task-3", + referenceTaskName: "child2__3", + workflowTask: { + name: "Child 2", + taskReferenceName: "child2", + type: "SIMPLE", + }, + iteration: 3, + }), + ]; + + const [groups, _items] = processTasksToGroupsAndItems( + tasks as unknown as ExecutionTask[], + {}, + ); + + expect(groups).toHaveLength(3); + const forkGroup = groups.find((g) => g.id === "fork_ref"); + expect(forkGroup?.nestedGroups).toEqual(["child1__3", "child2__3"]); + }); + }); + + describe("FORK_JOIN_DYNAMIC - Mixed Scenario", () => { + it("should handle mix of exact matches and iteration suffixes", () => { + const tasks = [ + createMockTask({ + taskId: "fork-task-1", + referenceTaskName: "fork_ref", + workflowTask: { + name: "Fork Task", + taskReferenceName: "fork_ref", + type: "FORK_JOIN_DYNAMIC", + }, + inputData: { + forkedTasks: ["exact_match", "needs_suffix"], + }, + iteration: 2, + }), + createMockTask({ + taskId: "exact-match-task", + referenceTaskName: "exact_match", + workflowTask: { + name: "Exact Match", + taskReferenceName: "exact_match", + type: "SIMPLE", + }, + }), + createMockTask({ + taskId: "needs-suffix-task", + referenceTaskName: "needs_suffix__2", + workflowTask: { + name: "Needs Suffix", + taskReferenceName: "needs_suffix", + type: "SIMPLE", + }, + iteration: 2, + }), + ]; + + const [groups, _items] = processTasksToGroupsAndItems( + tasks as unknown as ExecutionTask[], + {}, + ); + + expect(groups).toHaveLength(3); + const forkGroup = groups.find((g) => g.id === "fork_ref"); + // Should have exact match for "exact_match" and suffixed match for "needs_suffix" + expect(forkGroup?.nestedGroups).toEqual([ + "exact_match", + "needs_suffix__2", + ]); + }); + }); + + describe("FORK_JOIN_DYNAMIC - Missing Groups", () => { + it("should fallback to original taskId when neither exact nor suffixed ID exists", () => { + const tasks = [ + createMockTask({ + taskId: "fork-task-1", + referenceTaskName: "fork_ref", + workflowTask: { + name: "Fork Task", + taskReferenceName: "fork_ref", + type: "FORK_JOIN_DYNAMIC", + }, + inputData: { + forkedTasks: ["missing_task"], + }, + iteration: 2, + }), + // No matching tasks for "missing_task" or "missing_task__2" + ]; + + const [groups, _items] = processTasksToGroupsAndItems( + tasks as unknown as ExecutionTask[], + {}, + ); + + expect(groups).toHaveLength(1); + const forkGroup = groups.find((g) => g.id === "fork_ref"); + // Should fallback to original taskId when neither exact nor suffixed ID exists + expect(forkGroup?.nestedGroups).toEqual(["missing_task"]); + }); + }); + + describe("Edge Cases", () => { + it("should handle empty tasks array", () => { + const [groups, _items] = processTasksToGroupsAndItems([], {}); + + expect(groups).toHaveLength(0); + expect(_items).toHaveLength(0); + }); + + it("should handle tasks without startTime or endTime", () => { + const tasks = [ + createMockTask({ + startTime: 0, + endTime: 0, + }), + ]; + + const [groups, _items] = processTasksToGroupsAndItems( + tasks as unknown as ExecutionTask[], + {}, + ); + + expect(groups).toHaveLength(1); + expect(_items).toHaveLength(0); // No items should be created when no start/end time + }); + + it("should handle FORK_JOIN_DYNAMIC without inputData.forkedTasks", () => { + const tasks = [ + createMockTask({ + taskId: "fork-task-1", + referenceTaskName: "fork_ref", + workflowTask: { + name: "Fork Task", + taskReferenceName: "fork_ref", + type: "FORK_JOIN_DYNAMIC", + }, + inputData: {}, + }), + ]; + + const [groups, _items] = processTasksToGroupsAndItems( + tasks as unknown as ExecutionTask[], + {}, + ); + + expect(groups).toHaveLength(1); + const forkGroup = groups.find((g) => g.id === "fork_ref"); + expect(forkGroup?.nestedGroups).toBeUndefined(); + }); + + it("should handle FORK_JOIN_DYNAMIC with null inputData", () => { + const tasks = [ + createMockTask({ + taskId: "fork-task-1", + referenceTaskName: "fork_ref", + workflowTask: { + name: "Fork Task", + taskReferenceName: "fork_ref", + type: "FORK_JOIN_DYNAMIC", + }, + inputData: null, + }), + ]; + + const [groups, _items] = processTasksToGroupsAndItems( + tasks as unknown as ExecutionTask[], + {}, + ); + + expect(groups).toHaveLength(1); + const forkGroup = groups.find((g) => g.id === "fork_ref"); + expect(forkGroup?.nestedGroups).toBeUndefined(); + }); + + it("should handle tasks with only startTime", () => { + const tasks = [ + createMockTask({ + startTime: Date.now() - 1000, + endTime: 0, + }), + ]; + + const [groups, _items] = processTasksToGroupsAndItems( + tasks as unknown as ExecutionTask[], + {}, + ); + + expect(groups).toHaveLength(1); + expect(_items).toHaveLength(1); + expect(_items[0].start).toBeInstanceOf(Date); + expect(_items[0].end).toBeInstanceOf(Date); + }); + + it("should handle tasks with only endTime", () => { + const tasks = [ + createMockTask({ + startTime: 0, + endTime: Date.now(), + }), + ]; + + const [groups, _items] = processTasksToGroupsAndItems( + tasks as unknown as ExecutionTask[], + {}, + ); + + expect(groups).toHaveLength(1); + expect(_items).toHaveLength(1); + expect(_items[0].start).toBeInstanceOf(Date); + expect(_items[0].end).toBeInstanceOf(Date); + }); + }); + + describe("Complex Real-world Scenario", () => { + it("should handle a complex workflow with multiple FORK_JOIN_DYNAMIC tasks and iterations", () => { + const tasks = [ + // Main fork task + createMockTask({ + taskId: "main-fork", + referenceTaskName: "main_fork_ref", + workflowTask: { + name: "Main Fork", + taskReferenceName: "main_fork_ref", + type: "FORK_JOIN_DYNAMIC", + }, + inputData: { + forkedTasks: ["sub_task1", "sub_task2"], + }, + iteration: 1, + }), + // Sub fork task + createMockTask({ + taskId: "sub-fork", + referenceTaskName: "sub_fork_ref", + workflowTask: { + name: "Sub Fork", + taskReferenceName: "sub_fork_ref", + type: "FORK_JOIN_DYNAMIC", + }, + inputData: { + forkedTasks: ["nested_task1", "nested_task2"], + }, + iteration: 2, + }), + // Tasks with iteration suffixes + createMockTask({ + taskId: "sub-task1-1", + referenceTaskName: "sub_task1__1", + workflowTask: { + name: "Sub Task 1", + taskReferenceName: "sub_task1", + type: "SIMPLE", + }, + iteration: 1, + }), + createMockTask({ + taskId: "sub-task2-1", + referenceTaskName: "sub_task2__1", + workflowTask: { + name: "Sub Task 2", + taskReferenceName: "sub_task2", + type: "SIMPLE", + }, + iteration: 1, + }), + createMockTask({ + taskId: "nested-task1-2", + referenceTaskName: "nested_task1__2", + workflowTask: { + name: "Nested Task 1", + taskReferenceName: "nested_task1", + type: "SIMPLE", + }, + iteration: 2, + }), + createMockTask({ + taskId: "nested-task2-2", + referenceTaskName: "nested_task2__2", + workflowTask: { + name: "Nested Task 2", + taskReferenceName: "nested_task2", + type: "SIMPLE", + }, + iteration: 2, + }), + ]; + + const executionStatusMap = createMockExecutionStatusMap({ + main_fork_ref: { related: "some_parent" }, + sub_fork_ref: { related: "main_fork_ref" }, + }); + + const [groups, _items] = processTasksToGroupsAndItems( + tasks as unknown as ExecutionTask[], + executionStatusMap, + ); + + expect(groups).toHaveLength(6); + expect(_items).toHaveLength(6); + + // Test main fork nested groups + const mainForkGroup = groups.find((g) => g.id === "main_fork_ref"); + expect(mainForkGroup?.nestedGroups).toEqual([ + "sub_task1__1", + "sub_task2__1", + ]); + expect(mainForkGroup?.treeLevel).toBe(2); + + // Test sub fork nested groups + const subForkGroup = groups.find((g) => g.id === "sub_fork_ref"); + expect(subForkGroup?.nestedGroups).toEqual([ + "nested_task1__2", + "nested_task2__2", + ]); + expect(subForkGroup?.treeLevel).toBe(2); + }); + }); +}); diff --git a/ui-next/src/pages/execution/UpdateTaskStatusForm.tsx b/ui-next/src/pages/execution/UpdateTaskStatusForm.tsx new file mode 100644 index 0000000000..6ffeb29533 --- /dev/null +++ b/ui-next/src/pages/execution/UpdateTaskStatusForm.tsx @@ -0,0 +1,207 @@ +import { Box, Grid } from "@mui/material"; +import MenuItem from "@mui/material/MenuItem"; +import { Button, Text } from "components"; +import { ConductorSelect } from "components/v1"; +import { ConductorCodeBlockInput } from "components/v1/ConductorCodeBlockInput"; +import debounce from "lodash/debounce"; +import _isEmpty from "lodash/isEmpty"; +import sharedStyles from "pages/styles"; +import { ChangeEvent, FunctionComponent, useState } from "react"; + +const style = { + ...sharedStyles, + paper: { + margin: "20px", + padding: "20px", + }, + name: { + width: "50%", + }, + submitButton: { + float: "right", + }, + fields: { + display: "flex", + flexDirection: "column", + gap: "15px", + }, + controls: { + marginLeft: "15px", + marginTop: "20px", + height: "calc(100% - 83px)", + overflowY: "scroll", + width: "calc(100% - 15px)", + overflowX: "hidden", + paddingBottom: "60px", + }, + monaco: { + padding: "10px", + borderColor: "rgba(128, 128, 128, 0.2)", + borderStyle: "solid", + borderWidth: "1px", + borderRadius: "4px", + backgroundColor: "rgb(255, 255, 255)", + "&:focus-within": { + margin: "-2px", + borderColor: "rgb(73, 105, 228)", + borderStyle: "solid", + borderWidth: "2px", + }, + }, + labelText: { + position: "relative", + fontSize: "13px", + transform: "none", + fontWeight: 600, + paddingLeft: 0, + paddingBottom: "8px", + }, + inputBox: { + marginTop: "10px", + "& textarea": { + minWidth: "368px", + fontFamily: "monospace", + }, + "& input": { + minWidth: "368px", + }, + "& label": {}, + }, + roBox: { + marginTop: "10px", + "& .MuiOutlinedInput-root": { + background: "transparent", + border: "none", + }, + "& fieldset": { + border: "none", + }, + "& textarea": { + minWidth: "450px", + minHeight: "140px", + fontFamily: "monospace", + overflow: "none", + }, + "& input": { + minWidth: "368px", + }, + "& label": {}, + }, + cronApply: { + marginTop: "-12px", + "& svg": { + fontSize: "18px", + }, + }, + toggleButton: { + marginTop: "-12px", + "& svg": { + fontSize: "22px", + }, + }, + cronSample: { + fontSize: "12px", + height: "50px", + }, +}; + +const possibleTaskStatus = [ + "COMPLETED", + "FAILED", + "FAILED_WITH_TERMINAL_ERROR", +]; + +const taskMenuItems = possibleTaskStatus.map((n) => ( + + {n} + +)); + +interface UpdateTaskStatusFormProps { + onConfirm: (status: string, body: string) => void; +} + +export const UpdateTaskStatusForm: FunctionComponent< + UpdateTaskStatusFormProps +> = ({ onConfirm }) => { + const [selected, setSelected] = useState(""); + const [params, setParams] = useState("{}"); + const [isValidJson, setIsValidaJson] = useState(true); + + const parsedValue = debounce((params: string) => { + try { + const parsedValue = JSON.parse(params); + if (Array.isArray(parsedValue)) { + setIsValidaJson(false); + } else { + setIsValidaJson(true); + } + } catch { + setIsValidaJson(false); + } + }, 500); + + const handleSelectChange = ( + event: ChangeEvent, + ) => { + setSelected(event.target.value as string); + }; + + const handleInputChange = (val: string) => { + parsedValue(val); + setParams(val); + }; + + return ( + + + + + Update task + + + + + + Select Status + + {taskMenuItems} + + + + + + + + + + + ); +}; diff --git a/ui-next/src/pages/execution/WorkflowIntrospection.tsx b/ui-next/src/pages/execution/WorkflowIntrospection.tsx new file mode 100644 index 0000000000..9fcf2b4504 --- /dev/null +++ b/ui-next/src/pages/execution/WorkflowIntrospection.tsx @@ -0,0 +1,907 @@ +import React, { FunctionComponent, JSX, useContext, useState } from "react"; +import { Box, Stack, Tooltip } from "@mui/material"; +import { + DetailedTime, + ExecutionTask, + WorkflowExecution, + WorkflowIntrospectionRecord, +} from "types/Execution"; +import { ColorModeContext } from "theme/material/ColorModeContext"; +import { LegacyColumn } from "components/DataTable/types"; +import { colors } from "theme/tokens/variables"; +import { DataTable } from "components/index"; +import Dropdown from "components/Dropdown"; +import { clickHandler, taskIdRenderer } from "pages/execution/componentHelpers"; +import { StackTraceComponent } from "components/StackTrace"; + +interface WorkflowIntrospectionProps { + selectTask: (taskSel: { ref?: string; taskId?: string }) => void; + workflow: WorkflowExecution; +} + +function formatDetailedTime(time: DetailedTime) { + if (!time) return ""; + + const parts = []; + + const micros = Math.floor(time.nanos / 1_000); + const millis = Math.floor(micros / 1_000); + + if (Math.floor(time.seconds) > 0) { + parts.push(`${Math.floor(time.seconds)} seconds`); + } + + if (millis > 0) { + parts.push(`${millis}ms`); + } + + if (micros > 0) { + parts.push(`${micros % 1_000}μs`); + } + + return parts.join(" "); +} + +function compareDetailedTime( + a: DetailedTime, + b: DetailedTime, + ascending: boolean = true, +): number { + if (a.seconds !== b.seconds) { + return ascending ? a.seconds - b.seconds : b.seconds - a.seconds; + } else { + return ascending ? a.nanos - b.nanos : b.nanos - a.nanos; + } +} + +function nanos(time: DetailedTime) { + return time.seconds * 1e9 + time.nanos; +} + +function getColor( + colorMap: Map, + records: Map, + record: WorkflowIntrospectionRecord, +) { + const colorKey = `${record.id}-${record.threadName}`; + + if (colorMap.has(colorKey)) { + return colorMap.get(colorKey) || 123; + } else if (record.parentRecordId && records.has(record.parentRecordId)) { + return getColor( + colorMap, + records, + records.get(record.parentRecordId)!.record, + ); + } else { + return 123; + } +} + +class Tree { + roots: TreeNode[] = []; + records: Map = new Map(); + + add(root: TreeNode) { + this.roots.push(root); + this.records.set(root.record.id, root); + } +} + +function duration(nodes: TreeNode[]) { + let duration = 0; + + for (const node of nodes) { + duration = Math.max(duration, nanos(node.record.duration)); + } + + return duration; +} + +class TreeNode { + parent: TreeNode | null = null; + record: WorkflowIntrospectionRecord; + children: TreeNode[] = []; + overlapArr: TreeNode[]; + overlapSet = new Set(); + threadArray: string[] = []; + threadMap = new Map(); + + constructor(record: WorkflowIntrospectionRecord) { + this.record = record; + this.overlapArr = [this]; + this.overlapSet.add(this); + } + + sort() { + this.threadArray.sort((a, b) => { + const aNode = this.threadMap.get(a); + const bNode = this.threadMap.get(b); + + if (aNode && bNode) { + return compareDetailedTime( + aNode.record.duration, + bNode.record.duration, + false, + ); + } else if (aNode) { + return -1; + } else if (bNode) { + return 1; + } else { + return 0; + } + }); + } + + add(child: TreeNode) { + child.parent = this; + this.children.push(child); + + if (!this.threadMap.has(child.record.threadName)) { + this.threadMap.set(child.record.threadName, child); + this.threadArray.push(child.record.threadName); + } + + this.sort(); + } + + addOverlap(node: TreeNode) { + if (!this.overlapSet.has(node)) { + this.overlapArr.push(node); + this.overlapArr.sort((a, b) => + compareDetailedTime(a.record.duration, b.record.duration, false), + ); + } + + if (!node.overlapSet.has(this)) { + node.overlapArr.push(node); + node.overlapArr.sort((a, b) => + compareDetailedTime(a.record.duration, b.record.duration, false), + ); + } + } + + hasLeaf() { + if (this.record.attributes && this.record.attributes["isLeaf"] === true) { + return true; + } + + for (const thread of this.threadMap.values()) { + if (thread.hasLeaf()) { + return true; + } + } + + return false; + } + + size() { + let size = 1; + + for (const thread of this.threadMap.values()) { + size += thread.size(); + } + + return size; + } + + depth() { + const threadRecord = Object.groupBy( + this.overlapArr, + (node) => node.record.threadName, + ); + const threads: TreeNode[][] = []; + + for (const thread of Object.values(threadRecord)) { + if (thread) { + threads.push(thread); + } + } + + threads.sort((a, b) => duration(b) - duration(a)); + + const nodes: TreeNode[] = []; + + for (const thread of threads) { + nodes.push(...thread); + } + + return nodes.indexOf(this); + } +} + +export const WorkflowIntrospection: FunctionComponent< + WorkflowIntrospectionProps +> = ({ workflow, selectTask }) => { + const { mode } = useContext(ColorModeContext); + + let minTime = Number.POSITIVE_INFINITY; + let maxTime = Number.NEGATIVE_INFINITY; + + const tree = new Tree(); + + let leafDuration = 0; + const leafRanges: { start: number; end: number; node: TreeNode }[] = []; + const nodeRanges: { start: number; end: number; node: TreeNode }[] = []; + + for (const record of workflow.workflowIntrospection || []) { + const node = new TreeNode(record); + + tree.records.set(record.id, node); + nodeRanges.push({ + start: nanos(record.start), + end: nanos(record.start) + nanos(record.duration), + node: node, + }); + + if (record.attributes && record.attributes["isLeaf"] === true) { + leafRanges.push({ + start: nanos(record.start), + end: nanos(record.start) + nanos(record.duration), + node: node, + }); + } + + minTime = Math.min(minTime, nanos(record.start)); + maxTime = Math.max(maxTime, nanos(record.start) + nanos(record.duration)); + } + + // Shift all times to start at 0 + for (const leaf of leafRanges) { + leaf.start -= minTime; + leaf.end -= minTime; + } + + for (const range of nodeRanges) { + for (const other of nodeRanges) { + if (range === other) continue; + + if (range.start >= other.start && range.end <= other.end) { + // Leaf time is fully contained within another leaf time executing in parallel + range.node.addOverlap(other.node); + } else if (range.start < other.start && range.end >= other.start) { + // [------------------] <- leaf + // [------------------] <- other + // [-----] <- leaf (duration adjusted for overlap) + range.node.addOverlap(other.node); + } else if ( + range.start > other.start && + range.end >= other.end && + range.start < other.end + ) { + // [------------------] <- leaf + // [------------------] <- other + // [-----] <- leaf (duration adjusted for overlap) + range.node.addOverlap(other.node); + } else { + // Leaves are fully disjointed + } + } + } + + for (const leaf of leafRanges) { + for (const other of leafRanges) { + if (leaf === other || leaf.end === 0 || other.end === 0) continue; + + if (leaf.start >= other.start && leaf.end <= other.end) { + // Leaf time is fully contained within another leaf time executing in parallel + leaf.start = leaf.end = 0; + break; + } else if (leaf.start < other.start && leaf.end >= other.start) { + // [------------------] <- leaf + // [------------------] <- other + // [-----] <- leaf (duration adjusted for overlap) + other.start = leaf.start; + leaf.start = leaf.end = 0; + break; + } else if ( + leaf.start > other.start && + leaf.end >= other.end && + leaf.start < other.end + ) { + // [------------------] <- leaf + // [------------------] <- other + // [-----] <- leaf (duration adjusted for overlap) + other.end = leaf.end; + leaf.start = leaf.end = 0; + } else { + // Leaves are fully disjointed + } + } + } + + for (const leaf of leafRanges) { + leafDuration += Math.max(0, leaf.end - leaf.start); + } + + for (const record of workflow.workflowIntrospection || []) { + const node = tree.records.get(record.id)!; + + if (record.parentRecordId) { + const parent = tree.records.get(record.parentRecordId); + + if (parent) { + parent.add(node); + } else { + console.error( + `Parent record ${record.parentRecordId} not found for record ${record.id}`, + ); + } + } else { + tree.add(node); + } + } + const highlight = { + backgroundColor: "rgba(0, 0, 0, 0.1)", + }; + + const roots: JSX.Element[] = []; + + const [selected, setHovered] = React.useState(null); + const [hideShortOperations, setHideShortOperations] = useState( + "Hide Short Operations", + ); + + const totalDuration = maxTime - minTime; + const threadNames = new Map; arr: string[] }>(); + + let maxDepth = 0; + + for (const record of workflow.workflowIntrospection || []) { + const parent = tree.records.get(record.parentRecordId || ""); + + if (parent) { + if (!threadNames.has(parent.record.id)) { + threadNames.set(parent.record.id, { set: new Set(), arr: [] }); + } + + const entry = threadNames.get(parent.record.id)!; + + if (!entry.set.has(record.threadName)) { + entry.set.add(record.threadName); + entry.arr.push(record.threadName); + } + } + } + + const colorsMap = new Map(); + let colorCount = 0; + + for (const record of workflow.workflowIntrospection || []) { + if (record.parentRecordId) { + const parentThreads = threadNames.get(record.parentRecordId); + + if (parentThreads && parentThreads.set.size > 1) { + ++colorCount; + + colorsMap.set( + `${record.id}-${record.threadName}`, + 123 + colorCount++ * 13, + ); + } + } + } + + const rows = []; + const threads = new Map< + string, + { threadElements: JSX.Element[]; children: Map } + >(); + + for (const record of workflow.workflowIntrospection || []) { + const duration = nanos(record.duration); + const parent = tree.records.get(record.parentRecordId || "") || null; + const widthPercentage = (duration / totalDuration) * 100; + const node = tree.records.get(record.id)!; + + if ( + hideShortOperations === "Hide Short Operations" && + widthPercentage < 2 && + !node.hasLeaf() + ) { + continue; + } + + rows.push(record); + + const width = `${widthPercentage}%`; + + if (parent && !threads.has(parent.record.id)) { + threads.set(parent.record.id, { + threadElements: [], + children: new Map(), + }); + } + + if (!threads.has(record.id)) { + threads.set(record.id, { + threadElements: [], + children: new Map(), + }); + } + + const hover = (event: React.MouseEvent) => { + event.stopPropagation(); + + setHovered(record.id); + }; + + const hue = getColor(colorsMap, tree.records, record); + const backgroundColor = + selected === record.id + ? `hsl(${hue},14%,54%)` + : `hsl(${hue + 7},47%,74%)`; + const leftPercentage = (nanos(record.start) - minTime) / totalDuration; + + const depth = node.depth() || 0; + + maxDepth = Math.max(maxDepth, depth); + + const border = "1px solid rgba(0, 0, 0, 0.12"; + + const headerStyle = { + fontWeight: "bold", + margin: 0, + backgroundColor: mode === "dark" ? colors.gray04 : colors.gray14, + borderRight: border, + padding: "5px 10px 5px 10px", + whiteSpace: "nowrap", + }; + + const itemStyle = { + whiteSpace: "nowrap", + margin: 0, + padding: "5px 10px 5px 5px", + }; + + const hasDescription = + record.description && record.description.trim().length > 0; + + const borderRadius = "5px"; + + const attributes: JSX.Element[] = []; + + if (record.attributes) { + for (const [key, value] of Object.entries(record.attributes)) { + let headerText = key.replace(/([A-Z_])/g, " $1"); + + headerText = headerText.charAt(0).toUpperCase() + headerText.slice(1); + + attributes.push( + <> +

    {headerText}:

    +

    {String(value)}

    + , + ); + } + } + + const tooltip = ( +
    +

    + Name:{" "} +

    +

    {record.name}

    +

    ID:

    +

    {record.id}

    +

    Thread:

    +

    {record.threadName}

    + {attributes} +

    + Duration:{" "} +

    +

    {formatDetailedTime(record.duration)}

    +

    + {record.description} +

    +
    + ); + + const scroll = (event: React.MouseEvent) => { + event.stopPropagation(); + + document + .getElementById(`row-${record.id}`) + ?.scrollIntoView({ behavior: "smooth", block: "center" }); + }; + + const element = ( + +
    +

    + {record.name} +

    +

    + {formatDetailedTime(record.duration)} +

    +
    +
    + ); + + roots.push(element); + } + + const detailedFullDuration = { + seconds: Math.floor((maxTime - minTime) / 1e9), + nanos: (maxTime - minTime) % 1e9, + }; + + const detailedLeafDuration = { + seconds: Math.floor(leafDuration / 1e9), + nanos: leafDuration % 1e9, + }; + + const detailedOverheadDuration = { + seconds: Math.floor((maxTime - minTime - leafDuration) / 1e9), + nanos: Math.floor((maxTime - minTime - leafDuration) % 1e9), + }; + + const defaultStyle = { + transition: "0.1s ease", + padding: "7.5px", + }; + + const workflowIntrospectionFields: LegacyColumn[] = [ + { + id: "name", + name: "name", + label: "Name", + minWidth: "300px", + width: "300px", + maxWidth: "300px", + style: { + fontSize: "13px", + whiteSpace: "nowrap", + wordBreak: "keep-all", + flex: 1, + ...defaultStyle, + }, + tooltip: "The name of the operation Conductor is performing", + conditionalCellStyles: [], + }, + { + id: "id", + name: "id", + label: "ID", + maxWidth: "300px", + minWidth: "300px", + tooltip: "The unique identifier of the operation", + style: defaultStyle, + center: true, + conditionalCellStyles: [], + }, + { + id: "duration", + name: "duration", + label: "Duration", + width: "200px", + tooltip: "The duration of the operation", + renderer: formatDetailedTime, + sortFunction: ( + a: WorkflowIntrospectionRecord, + b: WorkflowIntrospectionRecord, + ) => compareDetailedTime(a.duration, b.duration), + style: { whiteSpace: "nowrap", ...defaultStyle }, + right: true, + conditionalCellStyles: [], + }, + { + id: "overhead", + name: "overhead", + label: "Overhead", + width: "200px", + tooltip: "The overhead time of the operation (excludes child durations)", + renderer: formatDetailedTime, + sortFunction: ( + a: WorkflowIntrospectionRecord, + b: WorkflowIntrospectionRecord, + ) => compareDetailedTime(a.overhead, b.overhead), + style: { whiteSpace: "nowrap", ...defaultStyle }, + right: true, + conditionalCellStyles: [], + }, + { + id: "threadName", + name: "threadName", + label: "Thread", + width: "200px", + tooltip: "A name for the logical thread that this operation was part of", + style: defaultStyle, + conditionalCellStyles: [], + }, + { + id: "parentRecordId", + name: "parentRecordId", + label: "Parent", + minWidth: "300px", + tooltip: "The parent of this operation", + style: defaultStyle, + center: true, + conditionalCellStyles: [], + }, + { + id: "taskId", + name: "taskId", + label: "Task ID", + tooltip: "The unique identifier of the task, if applicable", + renderer: (taskId: string, row: ExecutionTask) => { + if (!taskId || taskId.trim().length === 0) { + return ""; + } + + const renderer = taskIdRenderer( + clickHandler((task) => { + selectTask({ taskId: task.taskId }); + }), + ); + + return renderer.apply(renderer, [taskId, row]); + }, + center: true, + conditionalCellStyles: [], + }, + { + id: "stacktrace", + name: "stacktrace", + label: "Stack Trace", + tooltip: "The specific line of code that initiated this operation", + conditionalCellStyles: [], + format: (row: WorkflowIntrospectionRecord) => ( + + ), + }, + { + id: "description", + name: "description", + label: "Description", + maxWidth: "400px", + tooltip: "Additional information about the operation", + style: { flex: 1, ...defaultStyle }, + conditionalCellStyles: [], + format: (row: WorkflowIntrospectionRecord) => + row.description?.split("\n").map((line, _index) => ( +

    + {line} +

    + )), + }, + ]; + + const chartHeight = (maxDepth + 1) * 30; + + return ( + + +

    + Summary +

    +
    +

    Workflow Duration:

    +

    + {formatDetailedTime(detailedFullDuration)} +

    +

    + User Operation Duration: +

    +

    + {formatDetailedTime(detailedLeafDuration)} +

    +

    Conductor Overhead:

    +

    + {formatDetailedTime(detailedOverheadDuration)} +

    +
    +
    + +
    +
    + + setHovered(row.id)} + customActions={[ + { + if (typeof val === "string") { + setHideShortOperations(val); + } else { + console.warn("Expected string value from dropdown"); + } + }} + style={{ width: "225px" }} + />, + ]} + customStyles={{ + responsiveWrapper: { + style: { + maxHeight: "100%", + }, + }, + rows: { + highlightOnHoverStyle: highlight, + style: (row: WorkflowIntrospectionRecord) => ({ + backgroundColor: + selected === row.id ? highlight.backgroundColor : "inherit", + }), + }, + }} + columns={ + workflowIntrospectionFields.map((col) => ({ + ...col, + conditionalCellStyles: [ + { + when: (row: WorkflowIntrospectionRecord) => + selected === row.id, + style: { + backgroundColor: highlight.backgroundColor, + }, + }, + ], + })) as LegacyColumn[] + } + defaultShowColumns={["name", "overhead", "description", "threadName"]} + localStorageKey="workflowIntrospectionTable" + sortByDefault={false} + /> + +
    + ); +}; diff --git a/ui-next/src/pages/execution/componentHelpers.tsx b/ui-next/src/pages/execution/componentHelpers.tsx new file mode 100644 index 0000000000..2ceab0f6ef --- /dev/null +++ b/ui-next/src/pages/execution/componentHelpers.tsx @@ -0,0 +1,37 @@ +import { ExecutionTask } from "types/Execution"; +import ClipboardCopy from "components/ClipboardCopy"; +import { Link } from "@mui/material"; +import _isEmpty from "lodash/isEmpty"; + +export function taskIdRenderer(handleClick: (row: ExecutionTask) => void) { + return (taskId: string, row: ExecutionTask) => { + let defTaskDisplay = taskId; + if (taskId) { + defTaskDisplay = `${taskId.substring(0, 4)}..${taskId.substring( + taskId.length - 4, + )}`; + } + return ( + + { + handleClick(row); + }} + > + {defTaskDisplay} + + + ); + }; +} + +export function clickHandler( + handleSelectedTask: ((task: ExecutionTask) => void) | undefined, +) { + return function handleClick(row: ExecutionTask) { + if (!_isEmpty(row)) { + handleSelectedTask?.(row); + } + }; +} diff --git a/ui-next/src/pages/execution/helpers.test.ts b/ui-next/src/pages/execution/helpers.test.ts new file mode 100644 index 0000000000..e8a2be84e1 --- /dev/null +++ b/ui-next/src/pages/execution/helpers.test.ts @@ -0,0 +1,666 @@ +import { taskWithLatestIteration } from "./helpers"; + +const TASK_LIST_WITH_ITERATION = [ + { + taskType: "SET_VARIABLE", + status: "COMPLETED", + referenceTaskName: "set_variable_ref_1", + retryCount: 0, + seq: 1, + workflowInstanceId: "c110ba93-cf7b-11ee-8b2d-3e09f721958e", + workflowType: "packet_runner", + taskId: "c112dd74-cf7b-11ee-8b2d-3e09f721958e", + workflowTask: { + name: "set_variable_1", + taskReferenceName: "set_variable_ref_1", + }, + iteration: 0, + }, + { + taskType: "DO_WHILE", + status: "COMPLETED", + referenceTaskName: "do_while_ref", + retryCount: 0, + seq: 2, + taskDefName: "do_while", + workflowInstanceId: "c110ba93-cf7b-11ee-8b2d-3e09f721958e", + workflowType: "packet_runner", + taskId: "c115eab5-cf7b-11ee-8b2d-3e09f721958e", + callbackAfterSeconds: 0, + workflowTask: { + name: "do_while", + taskReferenceName: "do_while_ref", + type: "DO_WHILE", + }, + iteration: 3, + }, + { + taskType: "SUB_WORKFLOW", + status: "COMPLETED", + + referenceTaskName: "sub_workflow_packet_ref__1", + retryCount: 0, + seq: 3, + pollCount: 0, + taskDefName: "sub_workflow", + workflowInstanceId: "c110ba93-cf7b-11ee-8b2d-3e09f721958e", + workflowType: "packet_runner", + taskId: "c116d516-cf7b-11ee-8b2d-3e09f721958e", + workflowTask: { + name: "sub_workflow", + taskReferenceName: "sub_workflow_packet_ref", + + type: "SUB_WORKFLOW", + }, + iteration: 1, + subWorkflowId: "c1221fb7-cf7b-11ee-8b2d-3e09f721958e", + }, + { + taskType: "INLINE", + status: "COMPLETED", + referenceTaskName: "need_reprocess_ref__1", + retryCount: 0, + seq: 4, + taskDefName: "need_reprocess", + workflowTask: { + name: "need_reprocess", + taskReferenceName: "need_reprocess_ref", + type: "INLINE", + }, + iteration: 1, + }, + { + taskType: "SET_VARIABLE", + status: "COMPLETED", + referenceTaskName: "set_variable_ref__1", + retryCount: 0, + seq: 5, + taskDefName: "set_variable", + workflowInstanceId: "c110ba93-cf7b-11ee-8b2d-3e09f721958e", + workflowType: "packet_runner", + taskId: "742462cd-cf7c-11ee-aca0-2ee6bb644b90", + workflowTask: { + name: "set_variable", + taskReferenceName: "set_variable_ref", + type: "SET_VARIABLE", + }, + iteration: 1, + }, + { + taskType: "SUB_WORKFLOW", + status: "COMPLETED", + referenceTaskName: "sub_workflow_packet_ref__2", + retryCount: 0, + seq: 6, + taskDefName: "sub_workflow", + workflowInstanceId: "c110ba93-cf7b-11ee-8b2d-3e09f721958e", + workflowType: "packet_runner", + taskId: "7430e5ee-cf7c-11ee-aca0-2ee6bb644b90", + workflowTask: { + name: "sub_workflow", + taskReferenceName: "sub_workflow_packet_ref", + type: "SUB_WORKFLOW", + }, + iteration: 2, + subWorkflowId: "74337dff-cf7c-11ee-aca0-2ee6bb644b90", + }, + { + taskType: "INLINE", + status: "COMPLETED", + referenceTaskName: "need_reprocess_ref__2", + retryCount: 0, + seq: 7, + taskDefName: "need_reprocess", + workflowInstanceId: "c110ba93-cf7b-11ee-8b2d-3e09f721958e", + workflowType: "packet_runner", + taskId: "d80ae15b-cf7c-11ee-aca0-2ee6bb644b90", + workflowTask: { + name: "need_reprocess", + taskReferenceName: "need_reprocess_ref", + type: "INLINE", + }, + iteration: 2, + }, + { + taskType: "SET_VARIABLE", + status: "COMPLETED", + referenceTaskName: "set_variable_ref__2", + retryCount: 0, + seq: 8, + taskDefName: "set_variable", + workflowInstanceId: "c110ba93-cf7b-11ee-8b2d-3e09f721958e", + workflowType: "packet_runner", + taskId: "d810fbdc-cf7c-11ee-aca0-2ee6bb644b90", + callbackAfterSeconds: 0, + outputData: {}, + workflowTask: { + name: "set_variable", + taskReferenceName: "set_variable_ref", + type: "SET_VARIABLE", + }, + iteration: 2, + }, + { + taskType: "SUB_WORKFLOW", + status: "COMPLETED", + referenceTaskName: "sub_workflow_packet_ref__3", + retryCount: 0, + seq: 9, + taskDefName: "sub_workflow", + workflowInstanceId: "c110ba93-cf7b-11ee-8b2d-3e09f721958e", + workflowType: "packet_runner", + taskId: "d819fc8d-cf7c-11ee-aca0-2ee6bb644b90", + workflowTask: { + name: "sub_workflow", + taskReferenceName: "sub_workflow_packet_ref", + type: "SUB_WORKFLOW", + }, + iteration: 3, + }, + { + taskType: "INLINE", + status: "COMPLETED", + referenceTaskName: "need_reprocess_ref__3", + retryCount: 0, + seq: 10, + taskDefName: "need_reprocess", + workflowInstanceId: "c110ba93-cf7b-11ee-8b2d-3e09f721958e", + workflowType: "packet_runner", + taskId: "f4b4d514-cf7c-11ee-aca0-2ee6bb644b90", + callbackAfterSeconds: 0, + workflowTask: { + name: "need_reprocess", + taskReferenceName: "need_reprocess_ref", + type: "INLINE", + }, + iteration: 3, + }, + { + taskType: "SET_VARIABLE", + status: "COMPLETED", + referenceTaskName: "set_variable_ref__3", + retryCount: 0, + seq: 11, + taskDefName: "set_variable", + workflowInstanceId: "c110ba93-cf7b-11ee-8b2d-3e09f721958e", + workflowType: "packet_runner", + taskId: "f4b968f5-cf7c-11ee-aca0-2ee6bb644b90", + workflowTask: { + name: "set_variable", + taskReferenceName: "set_variable_ref", + type: "SET_VARIABLE", + }, + iteration: 3, + }, +]; + +const TASK_LIST_WITH_ITERATION_EXPECTED_RESULT = { + taskType: "SUB_WORKFLOW", + status: "COMPLETED", + referenceTaskName: "sub_workflow_packet_ref__3", + retryCount: 0, + seq: 9, + taskDefName: "sub_workflow", + workflowInstanceId: "c110ba93-cf7b-11ee-8b2d-3e09f721958e", + workflowType: "packet_runner", + taskId: "d819fc8d-cf7c-11ee-aca0-2ee6bb644b90", + workflowTask: { + name: "sub_workflow", + taskReferenceName: "sub_workflow_packet_ref", + type: "SUB_WORKFLOW", + }, + iteration: 3, +}; + +const TASK_LIST_WITH_RETRY = [ + { + taskType: "HTTP", + status: "FAILED", + referenceTaskName: "get_random_fact", + retryCount: 0, + seq: 1, + taskDefName: "get_random_fact", + workflowInstanceId: "cdca4c49-0a81-11ee-b464-f6926e7ad88b", + workflowType: "check_najeeb", + taskId: "cdcac17a-0a81-11ee-b464-f6926e7ad88b", + reasonForIncompletion: + "Failed to invoke HTTP task due to: java.net.UnknownHostException: sascsa: Name does not resolve", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-6985bdbc5-hlltg", + workflowTask: { + name: "get_random_fact", + taskReferenceName: "get_random_fact", + type: "HTTP", + }, + iteration: 0, + }, + { + taskType: "HTTP", + status: "FAILED", + referenceTaskName: "get_random_fact", + retryCount: 1, + seq: 2, + taskDefName: "get_random_fact", + retriedTaskId: "cdcac17a-0a81-11ee-b464-f6926e7ad88b", + workflowInstanceId: "cdca4c49-0a81-11ee-b464-f6926e7ad88b", + workflowType: "check_najeeb", + taskId: "522b4b45-cfab-11ee-b3bf-0e83e96d9c97", + reasonForIncompletion: + "Failed to invoke HTTP task due to: java.net.UnknownHostException: sascsa: Name does not resolve", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d78545974-kk5md", + workflowTask: { + name: "get_random_fact", + taskReferenceName: "get_random_fact", + type: "HTTP", + }, + iteration: 0, + }, + { + taskType: "HTTP", + status: "FAILED", + referenceTaskName: "get_random_fact", + retryCount: 2, + seq: 3, + taskDefName: "get_random_fact", + workflowInstanceId: "cdca4c49-0a81-11ee-b464-f6926e7ad88b", + workflowType: "check_najeeb", + taskId: "56333ef6-cfab-11ee-b3bf-0e83e96d9c97", + reasonForIncompletion: + "Failed to invoke HTTP task due to: java.net.UnknownHostException: sascsa", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d78545974-kk5md", + workflowTask: { + name: "get_random_fact", + taskReferenceName: "get_random_fact", + type: "HTTP", + }, + iteration: 0, + }, + { + taskType: "HTTP", + status: "FAILED", + referenceTaskName: "get_random_fact", + retryCount: 3, + seq: 4, + taskDefName: "get_random_fact", + workflowInstanceId: "cdca4c49-0a81-11ee-b464-f6926e7ad88b", + workflowType: "check_najeeb", + taskId: "5abb8637-cfab-11ee-b3bf-0e83e96d9c97", + reasonForIncompletion: + "Failed to invoke HTTP task due to: java.net.UnknownHostException: sascsa: Name does not resolve", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d78545974-kk5md", + workflowTask: { + name: "get_random_fact", + taskReferenceName: "get_random_fact", + type: "HTTP", + }, + iteration: 0, + }, + { + taskType: "HTTP", + status: "FAILED", + referenceTaskName: "get_random_fact", + retryCount: 4, + seq: 5, + taskDefName: "get_random_fact", + workflowInstanceId: "cdca4c49-0a81-11ee-b464-f6926e7ad88b", + workflowType: "check_najeeb", + taskId: "878bd848-cfab-11ee-b3bf-0e83e96d9c97", + reasonForIncompletion: + "Failed to invoke HTTP task due to: java.net.UnknownHostException: sascsa: Name does not resolve", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d78545974-kk5md", + workflowTask: { + name: "get_random_fact", + taskReferenceName: "get_random_fact", + type: "HTTP", + }, + iteration: 0, + }, + { + taskType: "HTTP", + status: "FAILED", + referenceTaskName: "get_random_fact", + retryCount: 5, + seq: 6, + taskDefName: "get_random_fact", + workflowInstanceId: "cdca4c49-0a81-11ee-b464-f6926e7ad88b", + workflowType: "check_najeeb", + taskId: "f7355aed-cfab-11ee-b3bf-0e83e96d9c97", + reasonForIncompletion: + "Failed to invoke HTTP task due to: java.net.UnknownHostException: sascsa: Name does not resolve", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d78545974-kk5md", + workflowTask: { + name: "get_random_fact", + taskReferenceName: "get_random_fact", + type: "HTTP", + }, + iteration: 0, + }, + { + taskType: "HTTP", + status: "FAILED", + referenceTaskName: "get_random_fact", + retryCount: 6, + seq: 7, + pollCount: 1, + taskDefName: "get_random_fact", + workflowType: "check_najeeb", + taskId: "c106da20-cfac-11ee-b3bf-0e83e96d9c97", + reasonForIncompletion: + "Failed to invoke HTTP task due to: java.net.UnknownHostException: sascsa: Name does not resolve", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d78545974-kk5md", + workflowTask: { + name: "get_random_fact", + taskReferenceName: "get_random_fact", + type: "HTTP", + }, + iteration: 0, + }, + { + taskType: "HTTP", + status: "FAILED", + referenceTaskName: "get_random_fact", + retryCount: 7, + seq: 8, + taskDefName: "get_random_fact", + workflowInstanceId: "cdca4c49-0a81-11ee-b464-f6926e7ad88b", + workflowType: "check_najeeb", + taskId: "c4c19831-cfac-11ee-b3bf-0e83e96d9c97", + reasonForIncompletion: + "Failed to invoke HTTP task due to: java.net.UnknownHostException: sascsa", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d78545974-kk5md", + workflowTask: { + name: "get_random_fact", + taskReferenceName: "get_random_fact", + type: "HTTP", + }, + iteration: 0, + }, +]; + +const TASK_LIST_WITH_RETRY_EXPECTED_RESULT = { + taskType: "HTTP", + status: "FAILED", + referenceTaskName: "get_random_fact", + retryCount: 7, + seq: 8, + taskDefName: "get_random_fact", + workflowInstanceId: "cdca4c49-0a81-11ee-b464-f6926e7ad88b", + workflowType: "check_najeeb", + taskId: "c4c19831-cfac-11ee-b3bf-0e83e96d9c97", + reasonForIncompletion: + "Failed to invoke HTTP task due to: java.net.UnknownHostException: sascsa", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d78545974-kk5md", + workflowTask: { + name: "get_random_fact", + taskReferenceName: "get_random_fact", + type: "HTTP", + }, + iteration: 0, +}; + +const TASK_LIST_WITH_ITERATION_AND_RETRY = [ + { + taskType: "DO_WHILE", + status: "CANCELED", + referenceTaskName: "do_while_ref", + retryCount: 0, + seq: 1, + taskDefName: "do_while", + workflowInstanceId: "aa7045d0-cfd2-11ee-b3bf-0e83e96d9c97", + workflowType: "najeeb_dowhile_iteration_test", + taskId: "aa710921-cfd2-11ee-b3bf-0e83e96d9c97", + callbackAfterSeconds: 0, + workflowTask: { + name: "do_while", + taskReferenceName: "do_while_ref", + type: "DO_WHILE", + }, + iteration: 3, + }, + { + taskType: "HTTP", + status: "CANCELED", + referenceTaskName: "http_ref__1", + retryCount: 0, + seq: 2, + pollCount: 1, + taskDefName: "http", + workflowInstanceId: "aa7045d0-cfd2-11ee-b3bf-0e83e96d9c97", + workflowType: "najeeb_dowhile_iteration_test", + taskId: "aa717e52-cfd2-11ee-b3bf-0e83e96d9c97", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d78545974-kk5md", + workflowTask: { + name: "http", + taskReferenceName: "http_ref", + type: "HTTP", + }, + iteration: 1, + }, + { + taskType: "WAIT", + status: "COMPLETED", + referenceTaskName: "wait_ref__1", + retryCount: 0, + seq: 3, + taskDefName: "wait", + workflowInstanceId: "aa7045d0-cfd2-11ee-b3bf-0e83e96d9c97", + workflowType: "najeeb_dowhile_iteration_test", + taskId: "aa849123-cfd2-11ee-b3bf-0e83e96d9c97", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d78545974-kk5md", + outputData: {}, + workflowTask: { + name: "wait", + taskReferenceName: "wait_ref", + type: "WAIT", + }, + iteration: 1, + }, + { + taskType: "HTTP", + status: "COMPLETED", + referenceTaskName: "http_ref__2", + retryCount: 0, + seq: 4, + pollCount: 1, + taskDefName: "http", + workflowInstanceId: "aa7045d0-cfd2-11ee-b3bf-0e83e96d9c97", + workflowType: "najeeb_dowhile_iteration_test", + taskId: "ad8463a4-cfd2-11ee-b3bf-0e83e96d9c97", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d78545974-kk5md", + workflowTask: { + name: "http", + taskReferenceName: "http_ref", + type: "HTTP", + }, + iteration: 2, + }, + { + taskType: "HTTP", + status: "CANCELED", + referenceTaskName: "http_ref__1", + retryCount: 5, + seq: 13, + taskDefName: "http", + workflowInstanceId: "aa7045d0-cfd2-11ee-b3bf-0e83e96d9c97", + workflowType: "najeeb_dowhile_iteration_test", + taskId: "4d44f049-cfd9-11ee-b3bf-0e83e96d9c97", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d78545974-kk5md", + workflowTask: { + name: "http", + taskReferenceName: "http_ref", + type: "HTTP", + }, + iteration: 1, + }, + { + taskType: "HTTP", + status: "COMPLETED", + referenceTaskName: "http_ref__1", + retryCount: 6, + seq: 14, + pollCount: 1, + taskDefName: "http", + workflowInstanceId: "aa7045d0-cfd2-11ee-b3bf-0e83e96d9c97", + workflowType: "najeeb_dowhile_iteration_test", + taskId: "ce5c1e6c-cfd9-11ee-b3bf-0e83e96d9c97", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d78545974-kk5md", + workflowTask: { + name: "http", + taskReferenceName: "http_ref", + type: "HTTP", + }, + iteration: 1, + }, + { + taskType: "HTTP", + status: "CANCELED", + referenceTaskName: "http_ref__3", + retryCount: 1, + seq: 15, + taskDefName: "http", + workflowInstanceId: "aa7045d0-cfd2-11ee-b3bf-0e83e96d9c97", + workflowType: "najeeb_dowhile_iteration_test", + taskId: "9d8b67e7-cfdb-11ee-b3bf-0e83e96d9c97", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d78545974-kk5md", + workflowTask: { + name: "http", + taskReferenceName: "http_ref", + type: "HTTP", + }, + iteration: 3, + }, + { + taskType: "HTTP", + status: "CANCELED", + referenceTaskName: "http_ref__3", + retryCount: 2, + seq: 16, + taskDefName: "http", + workflowInstanceId: "aa7045d0-cfd2-11ee-b3bf-0e83e96d9c97", + workflowType: "najeeb_dowhile_iteration_test", + taskId: "229ad252-cfdc-11ee-b3bf-0e83e96d9c97", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d78545974-kk5md", + workflowTask: { + name: "http", + taskReferenceName: "http_ref", + type: "HTTP", + }, + iteration: 3, + }, + { + taskType: "HTTP", + status: "CANCELED", + referenceTaskName: "http_ref__3", + retryCount: 3, + seq: 17, + taskDefName: "http", + workflowInstanceId: "aa7045d0-cfd2-11ee-b3bf-0e83e96d9c97", + workflowType: "najeeb_dowhile_iteration_test", + taskId: "824358d9-cfdc-11ee-b3bf-0e83e96d9c97", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d78545974-kk5md", + workflowTask: { + name: "http", + taskReferenceName: "http_ref", + type: "HTTP", + }, + iteration: 3, + }, + { + taskType: "HTTP", + status: "CANCELED", + referenceTaskName: "http_ref__3", + retryCount: 4, + seq: 18, + taskDefName: "http", + workflowInstanceId: "aa7045d0-cfd2-11ee-b3bf-0e83e96d9c97", + workflowType: "najeeb_dowhile_iteration_test", + taskId: "334a1a70-cfdd-11ee-b3bf-0e83e96d9c97", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d78545974-kk5md", + workflowTask: { + name: "http", + taskReferenceName: "http_ref", + type: "HTTP", + }, + iteration: 3, + }, + { + taskType: "HTTP", + status: "CANCELED", + referenceTaskName: "http_ref__3", + retryCount: 5, + seq: 19, + taskDefName: "http", + workflowInstanceId: "aa7045d0-cfd2-11ee-b3bf-0e83e96d9c97", + workflowType: "najeeb_dowhile_iteration_test", + taskId: "7bff3b62-cfdd-11ee-b3bf-0e83e96d9c97", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d78545974-kk5md", + workflowTask: { + name: "http", + taskReferenceName: "http_ref", + type: "HTTP", + }, + iteration: 3, + }, +]; +const TASK_LIST_WITH_ITERATIONS_AND_RETRY_EXPECTED_RESULT = { + taskType: "HTTP", + status: "CANCELED", + referenceTaskName: "http_ref__3", + retryCount: 5, + seq: 19, + taskDefName: "http", + workflowInstanceId: "aa7045d0-cfd2-11ee-b3bf-0e83e96d9c97", + workflowType: "najeeb_dowhile_iteration_test", + taskId: "7bff3b62-cfdd-11ee-b3bf-0e83e96d9c97", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d78545974-kk5md", + workflowTask: { + name: "http", + taskReferenceName: "http_ref", + type: "HTTP", + }, + iteration: 3, +}; + +describe("taskWithLatestIteration", () => { + it("return task executed latest - task list with iterations", () => { + const result = taskWithLatestIteration( + TASK_LIST_WITH_ITERATION as any, + "sub_workflow_packet_ref", + ); + expect(result).toEqual(TASK_LIST_WITH_ITERATION_EXPECTED_RESULT); + }); + it("return task executed latest - task list with retry", () => { + const result = taskWithLatestIteration( + TASK_LIST_WITH_RETRY as any, + "get_random_fact", + ); + expect(result).toEqual(TASK_LIST_WITH_RETRY_EXPECTED_RESULT); + }); + + it("return task executed latest - task list with iterations + retry", () => { + const result = taskWithLatestIteration( + TASK_LIST_WITH_ITERATION_AND_RETRY as any, + "http_ref__3", + ); + expect(result).toEqual(TASK_LIST_WITH_ITERATIONS_AND_RETRY_EXPECTED_RESULT); + }); +}); diff --git a/ui-next/src/pages/execution/helpers.ts b/ui-next/src/pages/execution/helpers.ts new file mode 100644 index 0000000000..5cbca7f237 --- /dev/null +++ b/ui-next/src/pages/execution/helpers.ts @@ -0,0 +1,57 @@ +import { ExecutionTask } from "types/Execution"; +import _nth from "lodash/nth"; +import { StatusMap } from "./state/StatusMapTypes"; +type SeqResult = { + seqNumber: number; + idx: number; +}; + +export const taskWithLatestIteration = ( + tasksList: ExecutionTask[] = [], + taskReferenceName = "", + taskId?: string, +) => { + const filteredTasks = tasksList.filter( + (task) => + task.workflowTask.taskReferenceName === taskReferenceName || + task.taskId === taskId || + task.referenceTaskName === taskReferenceName, + ); + + if (filteredTasks && filteredTasks.length === 1) { + // task without any retry/iteration + return _nth(filteredTasks, 0); + } else if (filteredTasks && filteredTasks.length > 1) { + const result = filteredTasks.reduce( + (acc: SeqResult, task, idx) => { + if (task.seq && acc.seqNumber < Number(task.seq)) { + return { seqNumber: Number(task.seq), idx }; + } + return acc; + }, + { seqNumber: 0, idx: -1 }, + ); + + if (result.idx > -1) { + return _nth(filteredTasks, result.idx); + } + } + return undefined; +}; + +export function findTaskFromExecutionStatusMapById( + mapObject: StatusMap, + id: string | null, +) { + const keys = Object.keys(mapObject); + + for (const key of keys) { + const item = mapObject[key]; + const found = item?.loopOver?.find((loopItem) => loopItem?.taskId === id); + if (found) { + return found; + } + } + + return null; // return null if not found +} diff --git a/ui-next/src/pages/execution/index.ts b/ui-next/src/pages/execution/index.ts new file mode 100644 index 0000000000..a86d6139d9 --- /dev/null +++ b/ui-next/src/pages/execution/index.ts @@ -0,0 +1,3 @@ +import { UpdateTaskStatusForm } from "./UpdateTaskStatusForm"; + +export { UpdateTaskStatusForm }; diff --git a/ui-next/src/pages/execution/state/FlowExecutionContext/FlowExecutionContext.tsx b/ui-next/src/pages/execution/state/FlowExecutionContext/FlowExecutionContext.tsx new file mode 100644 index 0000000000..ecb54a39b2 --- /dev/null +++ b/ui-next/src/pages/execution/state/FlowExecutionContext/FlowExecutionContext.tsx @@ -0,0 +1,8 @@ +import { createContext } from "react"; +import { FlowExecutionContextProviderProps } from "./types"; + +export const FlowExecutionContext = + createContext({ + onExpandDynamic: () => {}, + onCollapseDynamic: () => {}, + }); diff --git a/ui-next/src/pages/execution/state/FlowExecutionContext/FlowExecutionProvider.tsx b/ui-next/src/pages/execution/state/FlowExecutionContext/FlowExecutionProvider.tsx new file mode 100644 index 0000000000..43f134f42b --- /dev/null +++ b/ui-next/src/pages/execution/state/FlowExecutionContext/FlowExecutionProvider.tsx @@ -0,0 +1,11 @@ +import { FunctionComponent } from "react"; +import { FlowExecutionContext } from "./FlowExecutionContext"; +import { FlowExecutionContextProviderProps } from "./types"; + +export const FlowExecutionContextProvider: FunctionComponent< + FlowExecutionContextProviderProps +> = ({ children, onExpandDynamic, onCollapseDynamic }) => ( + + {children} + +); diff --git a/ui-next/src/pages/execution/state/FlowExecutionContext/index.ts b/ui-next/src/pages/execution/state/FlowExecutionContext/index.ts new file mode 100644 index 0000000000..dbc87828c6 --- /dev/null +++ b/ui-next/src/pages/execution/state/FlowExecutionContext/index.ts @@ -0,0 +1,2 @@ +export * from "./FlowExecutionContext"; +export * from "./FlowExecutionProvider"; diff --git a/ui-next/src/pages/execution/state/FlowExecutionContext/types.ts b/ui-next/src/pages/execution/state/FlowExecutionContext/types.ts new file mode 100644 index 0000000000..a45a39f695 --- /dev/null +++ b/ui-next/src/pages/execution/state/FlowExecutionContext/types.ts @@ -0,0 +1,7 @@ +import { ReactNode } from "react"; + +export interface FlowExecutionContextProviderProps { + onExpandDynamic: (name: string) => void; + onCollapseDynamic: (name: string) => void; + children?: ReactNode; +} diff --git a/ui-next/src/pages/execution/state/StatusMapTypes.ts b/ui-next/src/pages/execution/state/StatusMapTypes.ts new file mode 100644 index 0000000000..b4c003dc02 --- /dev/null +++ b/ui-next/src/pages/execution/state/StatusMapTypes.ts @@ -0,0 +1,14 @@ +import { ExecutionTask, TaskDef } from "types"; + +export interface DynamicForkRelations { + siblings: ExecutionTask[]; + parentTaskReferenceName: string; +} +export interface TypeStatusMap extends ExecutionTask { + loopOver: ExecutionTask[]; + related: DynamicForkRelations; + outputData?: Record; + parentLoop?: TaskDef; +} + +export type StatusMap = Record; diff --git a/ui-next/src/pages/execution/state/actions.ts b/ui-next/src/pages/execution/state/actions.ts new file mode 100644 index 0000000000..cc6548d2b0 --- /dev/null +++ b/ui-next/src/pages/execution/state/actions.ts @@ -0,0 +1,525 @@ +import { + assign, + send, + spawn, + DoneInvokeEvent, + forwardTo, + sendTo, + raise, + ActorRef, + pure, +} from "xstate"; +import { executionToWorkflowDef } from "./executionMapper"; +import { flowMachine } from "components/flow/state/machine"; +import { + FlowActionTypes, + FlowEvents, + ResetZoomPositionEvent, + SelectTaskWithTaskRefEvent, +} from "components/flow/state/types"; +import { TaskStatus, Execution, DoWhileSelection, ExecutionTask } from "types"; +import { gtagAbstract, flattenGtagObject } from "utils"; +import { + UpdateWfDefinitionEvent, + SelectNodeEvent, +} from "components/flow/state/types"; +import { + UpdateExecutionEvent, + ExecutionMachineContext, + ExpandDynamicTaskEvent, + CollapseDynamicTaskEvent, + ErrorSeverity, + ClearErrorEvent, + UpdateDurationEvent, + ChangeExecutionTabEvent, + ExecutionActionTypes, + FetchForLogsEvent, + ExecutionUpdatedEvent, + PersistErrorEvent, + UpdateVariablesEvent, + MessageSeverity, + SetDoWhileIterationEvent, + UpdateSelectedTaskEvent, + ToggleAssistantPanelEvent, +} from "./types"; +import { RightPanelContextEventTypes, RightPanelEvents } from "../RightPanel"; +import { + findTaskFromExecutionStatusMapById, + taskWithLatestIteration, +} from "../helpers"; +import { NodeData } from "reaflow"; + +const selectTaskByTaskReferenceName = ( + { execution }: ExecutionMachineContext, + taskReferenceName: string, +) => { + return taskWithLatestIteration(execution?.tasks, taskReferenceName); +}; + +const pendingTaskSelection = (task: any) => { + const result = { + ...task.executionData, + workflowTask: task, + }; + return result; +}; + +const executionToExecutionStatusExpand = ( + executionDef: any, + expandedDynamic: any, + doWhileSelection?: DoWhileSelection[], + selectedTask?: ExecutionTask, +) => { + const [workflowDefinition, executionStatusMap] = executionToWorkflowDef( + executionDef, + expandedDynamic, + doWhileSelection, + selectedTask, + ); + return { + execution: executionDef, + workflowDefinition, + executionStatusMap, + }; +}; + +export const updateExecution = assign< + ExecutionMachineContext, + DoneInvokeEvent +>((context, { data }) => { + const expandedExecution = executionToExecutionStatusExpand( + data, + context.expandedDynamic, + context.doWhileSelection, + ); + return expandedExecution; +}); + +export const updateExecutionMap = assign< + ExecutionMachineContext, + DoneInvokeEvent +>((context, _data) => { + const expandedExecution = executionToExecutionStatusExpand( + context.execution, + context.expandedDynamic, + context.doWhileSelection, + context.selectedTask, + ); + return expandedExecution; +}); + +export const instanciateFlow = assign({ + flowChild: (_ctx, _event) => spawn(flowMachine), +}); + +export const persistExecutionId = assign< + ExecutionMachineContext, + UpdateExecutionEvent +>({ + executionId: (__, { executionId }) => executionId, +}); + +export const sendResetZoomEventToFlow = sendTo< + ExecutionMachineContext, + ResetZoomPositionEvent, + ActorRef +>( + (context) => context.flowChild!, + () => { + return { + type: FlowActionTypes.RESET_ZOOM_POSITION, + }; + }, + { delay: 50 }, +); + +export const notifyFlowUpdates = send< + ExecutionMachineContext, + UpdateWfDefinitionEvent +>( + (ctx) => { + return { + type: FlowActionTypes.UPDATE_WF_DEFINITION_EVT, + workflow: ctx.workflowDefinition, + showPorts: false, + workflowExecutionStatus: ctx?.execution?.status, + }; + }, + { to: (context) => context.flowChild! }, +); +// Commenting out dont think we need this +/* export const selectNodeInFlow = send( */ +/* ({ selectedTask }) => { */ +/* return { */ +/* type: FlowActionTypes.SELECT_NODE_INTERNAL_EVT, */ +/* node: { id: selectedTask?.workflowTask?.taskReferenceName }, */ +/* }; */ +/* }, */ +/* { to: (context) => context.flowChild! } */ +/* ); */ + +export const nodeToTaskSelectionToPanel = send< + ExecutionMachineContext, + SelectNodeEvent +>( + (context, { node }) => { + const selectedTask = + node?.data?.task?.executionData?.status === TaskStatus.PENDING + ? pendingTaskSelection(node?.data?.task) + : selectTaskByTaskReferenceName( + context, + node?.data?.task?.taskReferenceName, + ); + return { + type: RightPanelContextEventTypes.SET_SELECTED_TASK, + selectedTask, + }; + }, + { to: "#_internal" }, +); // maps the event to event. in the same cycle https://github.com/statelyai/xstate/discussions/1847 + +export const taskToTaskSelectionToPanel = send< + ExecutionMachineContext, + SelectTaskWithTaskRefEvent +>( + (context, { node, exactTaskRef }) => { + const maybeTask = + context?.executionStatusMap && context?.executionStatusMap[node.id]; + const selectedTask = maybeTask?.loopOver?.find( + (item) => item.referenceTaskName === exactTaskRef, + ); + + return { + type: RightPanelContextEventTypes.SET_SELECTED_TASK, + selectedTask, + }; + }, + { to: "#_internal" }, +); + +type WrappedErrorMessage = { + originalError: { status: number }; + errorDetails: { message: string }; +}; +export const assignError = assign< + ExecutionMachineContext, + DoneInvokeEvent +>({ + error: (context, { data: { originalError, errorDetails } }) => { + switch (originalError.status) { + case 403: + return { + severity: ErrorSeverity.ERROR, + text: "You don't have permission to execute this action", + }; + default: + return { + severity: ErrorSeverity.ERROR, + text: errorDetails.message, + }; + } + }, +}); + +export const persistFlowError = assign< + ExecutionMachineContext, + PersistErrorEvent +>({ error: (_, errorObject) => errorObject }); + +export const clearError = assign({ + error: (_context, _) => undefined, + message: (_context, _) => undefined, +}); + +export const addToExpandedDynamic = assign< + ExecutionMachineContext, + ExpandDynamicTaskEvent +>({ + expandedDynamic: ({ expandedDynamic }, { taskReferenceName }) => + expandedDynamic.concat(taskReferenceName), +}); + +export const removeFromExpandedDynamic = assign< + ExecutionMachineContext, + CollapseDynamicTaskEvent +>({ + expandedDynamic: ({ expandedDynamic }, { taskReferenceName }) => + expandedDynamic.filter((n) => n !== taskReferenceName), +}); + +export const updateWorkflowDefinition = assign( + ({ + execution, + expandedDynamic, + doWhileSelection, + selectedTask, + }: ExecutionMachineContext) => { + return executionToExecutionStatusExpand( + execution, + expandedDynamic, + doWhileSelection, + selectedTask, + ); + }, +); + +export const persistCurrentTab = assign< + ExecutionMachineContext, + ChangeExecutionTabEvent +>({ + currentTab: (_context, { tab }) => tab, +}); + +export const updateExecutionDuration = assign< + ExecutionMachineContext, + UpdateDurationEvent +>((__context, event) => { + return { + duration: event?.duration, + countdownType: event?.countdownType, + isDisabledCountdown: event?.isDisabled, + }; +}); + +export const gtagEventLogger = ( + context: ExecutionMachineContext, + event: any, +) => { + const flattenEvent = flattenGtagObject(event, "event"); + const eventPrefix = `event_at_execution_${context?.executionId}_of_type_${event?.type}`; + gtagAbstract(eventPrefix, { + user_uuid: context.currentUserInfo?.uuid, + workflow_name: context?.workflowDefinition?.name, + user_performed_action: event?.type, + ...flattenEvent, + }); +}; +export const gtagErrorLogger = ( + context: ExecutionMachineContext, + event: any, +) => { + const flattenEvent = flattenGtagObject(event, "event"); + const eventPrefix = `error_at_execution_${context?.executionId}_of_type_${event?.type}`; + gtagAbstract(eventPrefix, { + user_uuid: context.currentUserInfo?.uuid, + workflow_name: context?.workflowDefinition?.name, + user_performed_action: event?.type, + ...flattenEvent, + }); +}; +export const startRenderingGtag = ( + context: ExecutionMachineContext, + event: any, +) => { + const flattenEvent = flattenGtagObject(event, `event`); + const prefix = `event_at_execution_${context?.executionId}_start_rendering_diagram_request`; + gtagAbstract(prefix, { + user_uuid: context.currentUserInfo?.uuid, + workflow_name: context?.workflowDefinition?.name, + user_performed_action: event?.type, + start_time: new Date().getTime(), + ...flattenEvent, + }); +}; + +export const finishRenderingGtag = ( + context: ExecutionMachineContext, + event: any, +) => { + const flattenEvent = flattenGtagObject(event, `event`); + const prefix = `event_at_execution_${context?.executionId}_finish_rendering_diagram_request}`; + gtagAbstract(prefix, { + user_uuid: context.currentUserInfo?.uuid, + workflow_name: context?.workflowDefinition?.name, + user_performed_action: event?.type, + end_time: new Date().getTime(), + ...flattenEvent, + }); +}; + +export const fetchForLogs = send( + (_context, _event) => { + return { + type: ExecutionActionTypes.FETCH_FOR_LOGS, + }; + }, +); +export const sendUpdatedExecution = sendTo< + ExecutionMachineContext, + ExecutionUpdatedEvent, + ActorRef +>("rightPanelMachine", (context) => ({ + type: RightPanelContextEventTypes.SET_UPDATED_EXECUTION, + execution: context.execution!, + executionStatusMap: context.executionStatusMap!, +})); + +export const forwardSelectionToPanel = forwardTo("rightPanelMachine"); + +export const raiseExecutionUpdated = raise( + ExecutionActionTypes.EXECUTION_UPDATED, +); + +export const persistSuccessUpdateVariablesMessage = assign< + ExecutionMachineContext, + DoneInvokeEvent +>({ + message: (_context, _event) => { + return { + severity: MessageSeverity.SUCCESS, + text: "Variables updated successfully.", + }; + }, +}); + +export const persistDoWhileIteration = assign( + ( + { doWhileSelection }: ExecutionMachineContext, + { data }: SetDoWhileIterationEvent, + ) => { + const updatedDoWhileSelection = [...(doWhileSelection ?? [])]; + const index = doWhileSelection?.findIndex( + (item) => item.doWhileTaskReferenceName === data.doWhileTaskReferenceName, + ); + if (index != null && index !== -1) { + updatedDoWhileSelection[index] = data; + } else { + updatedDoWhileSelection.push(data); + } + return { + doWhileSelection: updatedDoWhileSelection, + }; + }, +); + +export const updateSelectedTask = assign( + (_context, data: UpdateSelectedTaskEvent) => { + return { + selectedTask: data.selectedTask, + }; + }, +); + +export const toggleAssistantPanel = assign< + ExecutionMachineContext, + ToggleAssistantPanelEvent +>({ + isAssistantPanelOpen: (context) => !context.isAssistantPanelOpen, +}); + +export const closeAssistantPanel = assign({ + isAssistantPanelOpen: () => false, +}); + +export const delayedNodeSelection = pure((ctx: ExecutionMachineContext) => { + const identifyNodeTobeSelected = ( + maybeSelectedTask?: ExecutionTask, + maybeSelectedNodeUsingTaskReference?: { id?: string }, + ) => { + const taskReferenceNameFromMaybeSelectedTask = + maybeSelectedTask?.workflowTask?.taskReferenceName; + const taskReferenceNameFromMaybeSelectedNodeUsingTaskReference = + maybeSelectedNodeUsingTaskReference?.id; + if ( + taskReferenceNameFromMaybeSelectedTask === + taskReferenceNameFromMaybeSelectedNodeUsingTaskReference + ) { + return { + nodeRef: taskReferenceNameFromMaybeSelectedTask, + exactTaskRef: maybeSelectedTask?.referenceTaskName, + }; + } else if (!maybeSelectedTask && maybeSelectedNodeUsingTaskReference) { + return { + nodeRef: taskReferenceNameFromMaybeSelectedNodeUsingTaskReference, + exactTaskRef: taskReferenceNameFromMaybeSelectedNodeUsingTaskReference, + }; + } else { + return { + nodeRef: taskReferenceNameFromMaybeSelectedTask, + exactTaskRef: maybeSelectedTask?.referenceTaskName, + }; + } + }; + let selectedTaskReferenceName = ctx.selectedTaskReferenceName; + if ( + ctx?.executionStatusMap && + (ctx?.selectedTaskId || ctx?.selectedTaskReferenceName) + ) { + const maybeSelectedTask = findTaskFromExecutionStatusMapById( + ctx?.executionStatusMap, + ctx?.selectedTaskId ?? "", + ); + + const { nodeRef, exactTaskRef } = identifyNodeTobeSelected( + maybeSelectedTask!, + { id: maybeSelectedTask?.workflowTask?.taskReferenceName }, + ); + if (exactTaskRef && nodeRef !== exactTaskRef) { + return [ + sendTo(ctx.flowChild!, { + type: FlowActionTypes.SELECT_TASK_WITH_TASK_REF, + node: { + id: maybeSelectedTask?.workflowTask?.taskReferenceName, + } as NodeData, + exactTaskRef, + }), + send((_context, _event) => { + return { + type: ExecutionActionTypes.UPDATE_QUERY_PARAM, + taskReferenceName: + maybeSelectedTask?.workflowTask?.taskReferenceName, + }; + }), + ]; + } + if (maybeSelectedTask) { + return [ + sendTo( + ctx.flowChild!, + { + type: FlowActionTypes.SELECT_NODE_EVT, + node: { + id: maybeSelectedTask?.workflowTask.taskReferenceName, + }, + }, + { + delay: 150, + id: "debounce_delayed_node_selection", + }, + ), + ]; + } + } + const selectedTask = + ctx.selectedTaskId && + ctx.execution?.tasks?.find((t) => t.taskId === ctx.selectedTaskId); + + // If reference name is not set, use the task id to get the reference name + if (!ctx.selectedTaskReferenceName && selectedTask) { + selectedTaskReferenceName = selectedTask.workflowTask.taskReferenceName; + } + + // This will prevent opening the right panel for wrong reference name + const selectedTaskExists = + (ctx.execution?.tasks ?? []).filter( + (t) => t.workflowTask.taskReferenceName === selectedTaskReferenceName, + ).length > 0; + + return selectedTaskExists + ? [ + sendTo( + ctx.flowChild!, + { + type: FlowActionTypes.SELECT_NODE_EVT, + node: { + id: selectedTaskReferenceName!, + }, + }, + { + delay: 150, + id: "debounce_delayed_node_selection", + }, + ), + ] + : []; +}); diff --git a/ui-next/src/pages/execution/state/constants.ts b/ui-next/src/pages/execution/state/constants.ts new file mode 100644 index 0000000000..644c27f913 --- /dev/null +++ b/ui-next/src/pages/execution/state/constants.ts @@ -0,0 +1,7 @@ +// Tabs +export const SUMMARY_TAB = 0; +export const INPUT_TAB = 1; +export const OUTPUT_TAB = 2; +export const LOGS_TAB = 3; +export const JSON_TAB = 4; +export const DEFINITION_TAB = 5; diff --git a/ui-next/src/pages/execution/state/countdownActions.ts b/ui-next/src/pages/execution/state/countdownActions.ts new file mode 100644 index 0000000000..931b6e3608 --- /dev/null +++ b/ui-next/src/pages/execution/state/countdownActions.ts @@ -0,0 +1,39 @@ +import { assign, sendParent } from "xstate"; +import { + COUNT_DOWN_TYPE, + ExecutionActionTypes, + CountdownContext, + UpdateDurationEvent, +} from "./types"; + +export const updateCountdownDuration = assign< + CountdownContext, + UpdateDurationEvent +>({ + duration: (ctx: CountdownContext, event) => event?.duration || 30, +}); + +export const resetCountdownElapsed = assign({ + elapsed: 0, +}); + +export const updateCountdownType = (type: COUNT_DOWN_TYPE) => + assign({ + countdownType: type, + }); + +export const updateParentDuration = sendParent( + (_ctx: CountdownContext, event: any) => ({ + type: ExecutionActionTypes.UPDATE_DURATION, + duration: event?.duration, + countdownType: event?.countdownType, + }), +); + +export const updateParentIsDisabled = (isDisabled = false) => + sendParent((ctx: CountdownContext) => ({ + type: ExecutionActionTypes.UPDATE_DURATION, + duration: ctx?.duration, + countdownType: ctx?.countdownType, + isDisabled: isDisabled, + })); diff --git a/ui-next/src/pages/execution/state/countdownMachine.ts b/ui-next/src/pages/execution/state/countdownMachine.ts new file mode 100644 index 0000000000..c7ba051d73 --- /dev/null +++ b/ui-next/src/pages/execution/state/countdownMachine.ts @@ -0,0 +1,135 @@ +import { assign, createMachine } from "xstate"; +import { COUNT_DOWN_TYPE } from "pages/execution/state/types"; +import { + updateParentIsDisabled, + resetCountdownElapsed, + updateCountdownDuration, + updateCountdownType, + updateParentDuration, +} from "pages/execution/state/countdownActions"; +import { + CountdownEventTypes, + CountdownContext, + CountdownEvents, +} from "./types"; + +const actions = { + resetCountdownElapsed, + updateCountdownDuration, + updateCountdownType, + updateParentDuration, + updateInfinityCountdownType: updateCountdownType(COUNT_DOWN_TYPE.INFINITE), + refreshImmediatelyWhileDisabled: updateParentIsDisabled(true), + enableCountdown: updateParentIsDisabled(false), +} as any; + +export const countdownMachine = createMachine< + CountdownContext, + CountdownEvents +>( + { + id: "countdownMachine", + context: { + duration: 30, + elapsed: 0, + executionStatus: "", + countdownType: COUNT_DOWN_TYPE.INFINITE, + isDisabled: false, + }, + initial: "running", + states: { + running: { + invoke: { + src: (_context) => (sp) => { + const interval = setInterval(() => { + sp(CountdownEventTypes.TICK); + }, 1000); + return () => { + clearInterval(interval); + }; + }, + }, + always: [ + { + target: "disabled", + cond: (ctx: CountdownContext) => !!ctx?.isDisabled, + }, + { + target: "finish", + cond: (ctx: CountdownContext) => ctx?.elapsed >= ctx?.duration, + }, + ], + on: { + [CountdownEventTypes.TICK]: { + actions: assign({ + elapsed: (ctx: any) => ctx.elapsed + 1, + }) as any, + }, + [CountdownEventTypes.DISABLE]: { + actions: ["resetCountdownElapsed"], + target: "disabled", + }, + [CountdownEventTypes.FORCE_FINISH]: { + actions: ["refreshImmediately"], + target: "finish", + }, + [CountdownEventTypes.UPDATE_DURATION]: { + actions: [ + "updateParentDuration", + "updateCountdownDuration", + "resetCountdownElapsed", + ], + }, + }, + }, + disabled: { + on: { + [CountdownEventTypes.ENABLE]: { + actions: [ + assign({ + isDisabled: false, + }) as any, + "enableCountdown", + ], + target: "running", + }, + [CountdownEventTypes.FORCE_FINISH]: { + actions: ["refreshImmediatelyWhileDisabled"], + target: "finish", + }, + }, + }, + idle: { + on: { + [CountdownEventTypes.UPDATE_DURATION]: { + actions: [ + "updateParentDuration", + "updateCountdownDuration", + "resetCountdownElapsed", + "updateInfinityCountdownType", + ], + target: "running", + }, + [CountdownEventTypes.ENABLE]: { + actions: ["updateInfinityCountdownType", "enableCountdown"], + target: "running", + }, + [CountdownEventTypes.DISABLE]: { + actions: ["updateInfinityCountdownType"], + target: "disabled", + }, + [CountdownEventTypes.FORCE_FINISH]: { + actions: ["refreshImmediately"], + target: "finish", + }, + }, + }, + finish: { + type: "final", + }, + }, + }, + { + actions, + }, +); diff --git a/ui-next/src/pages/execution/state/executionMapper.test.js b/ui-next/src/pages/execution/state/executionMapper.test.js new file mode 100644 index 0000000000..1590f12667 --- /dev/null +++ b/ui-next/src/pages/execution/state/executionMapper.test.js @@ -0,0 +1,443 @@ +import { + executionTasksToStatusMap, + taskStatusUpdater, + relatedNamesToTaskDef, + executionToWorkflowDef, + doWhileSelectionForStatusMap, +} from "./executionMapper"; +import { + sampleExecution, + sampleExecutionWithForkJoin, + newDynamicForkSample, + sampleExecutionDoWhile, + sampleExecutionMultiDoWhile, + sampleStatusMap, + doWhileSelectionForStatusMapResultSingleDoWhile, + doWhileSelectionForStatusMapResultMultiDowhile, +} from "./sampleExecutions"; + +describe("executionToWorkflowDef", () => { + describe("executionTasksToStatusMap", () => { + it("Should build a map will all execution tasks and selected task", () => { + const sampleExecutedObject = { + ref: "get_weather_ref", + taskId: "5c61b913-5883-4725-8d39-0edd3029cbaf", + }; + const builtMap = executionTasksToStatusMap(sampleExecution.tasks); + expect(Object.keys(builtMap)).toEqual( + expect.arrayContaining(["get_IP_ref", "get_weather_ref"]), + ); + expect(builtMap[sampleExecutedObject.ref].taskId).toEqual( + sampleExecutedObject.taskId, + ); + }); + }); + describe("taskStatusUpdater", () => { + it("Should return tasks with an executionData object", () => { + const sampleMap = { + get_IP_ref: { status: "COMPLETED", executed: true, loopOver: [] }, + get_weather_ref: { status: "FAILED", executed: false, loopOver: [] }, + }; + const sampleExecutionTasks = sampleExecution.workflowDefinition.tasks; + const result = taskStatusUpdater(sampleExecutionTasks, sampleMap); + expect( + result.every(({ executionData }) => executionData != null), + ).toEqual(true); + }); + }); + describe("relatedNamesToTaskDef", () => { + it("Should return a map of forked tasks and its siblings", () => { + const taskNames = [ + "shipping_loop_subworkflow_ref_0", + "shipping_loop_subworkflow_ref_1", + ]; + const result = relatedNamesToTaskDef( + taskNames, + sampleExecutionWithForkJoin.tasks, + ); + expect(Object.keys(result)).toEqual(taskNames); + }); + }); + + describe("return collapsedTasksStatus array and collapsedTasksStatus", () => { + it("should return collapsedTasksStatus array", () => { + const sampleMap = { + dynamic_ref: { + taskType: "HTTP", + status: "COMPLETED", + loopOver: [], + inputData: { + forkedTaskDefs: [ + { + taskReferenceName: "image_convert_resize_png_300x300_0", + }, + { + taskReferenceName: "image_convert_resize_png_200x200_1", + }, + { + taskReferenceName: "fallsas", + }, + ], + }, + }, + fallsas: { + taskType: "HTTP", + status: "COMPLETED", + loopOver: [], + }, + image_convert_resize_png_200x200_1: { + status: "CANCELED", + loopOver: [], + }, + image_convert_resize_png_300x300_0: { + status: "FAILED", + loopOver: [], + }, + join_task_ref: { + status: "CANCELED", + loopOver: [], + }, + }; + const sampleExecutionTasks = newDynamicForkSample.tasks; + const result = taskStatusUpdater(sampleExecutionTasks, sampleMap, []); + expect( + result[0].forkTasks[0][0].executionData.collapsedTasksStatus, + ).toEqual(["FAILED", "CANCELED", "COMPLETED"]); + }); + it("should return collapsedTasksStatus(card bundle) as FAILED", () => { + const sampleMap = { + dynamic_ref: { + taskType: "HTTP", + status: "COMPLETED", + loopOver: [], + inputData: { + forkedTaskDefs: [ + { + taskReferenceName: "image_convert_resize_png_300x300_0", + }, + { + taskReferenceName: "image_convert_resize_png_200x200_1", + }, + { + taskReferenceName: "fallsas", + }, + ], + }, + }, + fallsas: { + taskType: "HTTP", + status: "COMPLETED", + loopOver: [], + }, + image_convert_resize_png_200x200_1: { + status: "CANCELED", + loopOver: [], + }, + image_convert_resize_png_300x300_0: { + status: "FAILED", + loopOver: [], + }, + }; + const sampleExecutionTasks = newDynamicForkSample.tasks; + const result = taskStatusUpdater(sampleExecutionTasks, sampleMap, []); + expect(result[0].forkTasks[0][0].executionData.status).toEqual("FAILED"); + }); + it("should return collapsedTasksStatus(card bundle) as COMPLETED", () => { + const sampleMap = { + dynamic_ref: { + taskType: "HTTP", + status: "COMPLETED", + loopOver: [], + inputData: { + forkedTaskDefs: [ + { + taskReferenceName: "image_convert_resize_png_300x300_0", + }, + { + taskReferenceName: "image_convert_resize_png_200x200_1", + }, + { + taskReferenceName: "fallsas", + }, + ], + }, + }, + fallsas: { + taskType: "HTTP", + status: "COMPLETED", + loopOver: [], + }, + image_convert_resize_png_200x200_1: { + status: "COMPLETED", + loopOver: [], + }, + image_convert_resize_png_300x300_0: { + status: "COMPLETED", + loopOver: [], + }, + }; + const sampleExecutionTasks = newDynamicForkSample.tasks; + const result = taskStatusUpdater(sampleExecutionTasks, sampleMap, []); + expect(result[0].forkTasks[0][0].executionData.status).toEqual( + "COMPLETED", + ); + }); + it("should return collapsedTasksStatus(card bundle) as TIMED_OUT", () => { + const sampleMap = { + dynamic_ref: { + taskType: "HTTP", + status: "COMPLETED", + loopOver: [], + inputData: { + forkedTaskDefs: [ + { + taskReferenceName: "image_convert_resize_png_300x300_0", + }, + { + taskReferenceName: "image_convert_resize_png_200x200_1", + }, + { + taskReferenceName: "fallsas", + }, + ], + }, + }, + fallsas: { + taskType: "HTTP", + status: "COMPLETED", + loopOver: [], + }, + image_convert_resize_png_200x200_1: { + status: "COMPLETED", + loopOver: [], + }, + image_convert_resize_png_300x300_0: { + status: "TIMED_OUT", + loopOver: [], + }, + }; + const sampleExecutionTasks = newDynamicForkSample.tasks; + const result = taskStatusUpdater(sampleExecutionTasks, sampleMap, []); + expect(result[0].forkTasks[0][0].executionData.status).toEqual( + "TIMED_OUT", + ); + }); + }); + + describe("executionToWorkflowDef with doWhileSelection", () => { + it("Should build a map with given iteration - 4", () => { + const selectedIteration = 4; + const doWhileSelection = [ + { + doWhileTaskReferenceName: "do_while_ref", + selectedIteration: selectedIteration, + }, + ]; + + const [_workflowDefinition, executionStatusMap] = executionToWorkflowDef( + sampleExecutionDoWhile, + [], + doWhileSelection, + ); + expect(Object.keys(executionStatusMap)).toEqual([ + "http_ref_first", + "do_while_ref", + "switch_ref", + "http_second_ref", + "http_ref_four_ref", + "http_last_ref", + ]); + expect(executionStatusMap["switch_ref"].iteration).toEqual( + selectedIteration, + ); + expect(executionStatusMap["http_second_ref"].iteration).toEqual( + selectedIteration, + ); + expect(executionStatusMap["http_ref_four_ref"].iteration).toEqual( + selectedIteration, + ); + }); + it("Should build a map with given iteration - 1", () => { + const selectedIteration = 1; + const doWhileSelection = [ + { + doWhileTaskReferenceName: "do_while_ref", + selectedIteration: selectedIteration, + }, + ]; + + const [_workflowDefinition, executionStatusMap] = executionToWorkflowDef( + sampleExecutionDoWhile, + [], + doWhileSelection, + ); + expect(Object.keys(executionStatusMap)).toEqual([ + "http_ref_first", + "do_while_ref", + "switch_ref", + "http_third_ref", + "http_ref_cool", + "http_last_ref", + ]); + expect(executionStatusMap["switch_ref"].iteration).toEqual( + selectedIteration, + ); + expect(executionStatusMap["http_third_ref"].iteration).toEqual( + selectedIteration, + ); + expect(executionStatusMap["http_ref_cool"].iteration).toEqual( + selectedIteration, + ); + }); + it("Should build a map with muliti-dowhile given iteration 1,4", () => { + const selectedIterationForFirstDoWhile = 1; + const selectedIterationForSecondDoWhile = 4; + const doWhileSelection = [ + { + doWhileTaskReferenceName: "do_while_ref", + selectedIteration: selectedIterationForFirstDoWhile, + }, + { + doWhileTaskReferenceName: "do_while_ref_1", + selectedIteration: selectedIterationForSecondDoWhile, + }, + ]; + + const [_workflowDefinition, executionStatusMap] = executionToWorkflowDef( + sampleExecutionMultiDoWhile, + [], + doWhileSelection, + ); + expect(Object.keys(executionStatusMap)).toEqual( + expect.arrayContaining([ + "do_while_ref", + "http_ref_five", + "http_last_ref", + "do_while_ref_1", + "http_new_dowhile_one_ref", + "http_ref_first", + "switch_ref", + "switch_ref_1", + ]), + ); + // for the first doWhile + expect(executionStatusMap["switch_ref"].iteration).toEqual( + selectedIterationForFirstDoWhile, + ); + expect(executionStatusMap["http_ref_five"].iteration).toEqual( + selectedIterationForFirstDoWhile, + ); + + // for the second doWhile + expect(executionStatusMap["switch_ref_1"].iteration).toEqual( + selectedIterationForSecondDoWhile, + ); + expect(executionStatusMap["http_new_dowhile_one_ref"].iteration).toEqual( + selectedIterationForSecondDoWhile, + ); + }); + }); + + describe("executionToWorkflowDef without doWhileSelection", () => { + it("Should build expected map", () => { + const [_workflowDefinition, executionStatusMap] = executionToWorkflowDef( + sampleExecutionDoWhile, + [], + ); + expect(Object.keys(executionStatusMap)).toEqual([ + "http_ref_first", + "do_while_ref", + "switch_ref", + "http_third_ref", + "http_ref_cool", + "http_second_ref", + "http_ref_four_ref", + "http_ref_five", + "http_last_ref", + ]); + }); + }); +}); + +describe("doWhileSelectionForStatusMap", () => { + it("function works normally, returning expected result - single DoWhile selected", () => { + const doWhileSelection = [ + { + doWhileTaskReferenceName: "do_while_ref", + selectedIteration: 1, + }, + ]; + const result = doWhileSelectionForStatusMap( + doWhileSelection, + sampleStatusMap, + ); + expect(result).toEqual(doWhileSelectionForStatusMapResultSingleDoWhile); + }); + it("function works normally, returning expected result - multiple Dowhile selected", () => { + const doWhileSelection = [ + { + doWhileTaskReferenceName: "do_while_ref", + selectedIteration: 1, + }, + { + doWhileTaskReferenceName: "do_while_ref_1", + selectedIteration: 6, + }, + ]; + const result = doWhileSelectionForStatusMap( + doWhileSelection, + sampleStatusMap, + ); + expect(result).toEqual(doWhileSelectionForStatusMapResultMultiDowhile); + }); + it("should handle missing referenced task gracefully (currentAssociatedTask is undefined)", () => { + const doWhileSelection = [ + { + doWhileTaskReferenceName: "do_while_ref", + selectedIteration: 1, + }, + ]; + // statusMap is missing a referenced task for this iteration + const minimalStatusMap = { + do_while_ref: { + taskType: "DO_WHILE", + status: "COMPLETED", + inputData: { number: 10 }, + referenceTaskName: "do_while_ref", + retryCount: 0, + seq: 2, + pollCount: 0, + taskDefName: "do_while", + scheduledTime: 1, + startTime: 1, + endTime: 2, + updateTime: 2, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "wf1", + workflowType: "test", + taskId: "tid1", + callbackAfterSeconds: 0, + outputData: { + 1: { + missing_ref: { some: "data" }, + }, + }, + loopOver: [], + }, + // Note: 'missing_ref' is not present in the statusMap + }; + const result = doWhileSelectionForStatusMap( + doWhileSelection, + minimalStatusMap, + ); + // Should include 'missing_ref' with undefined or empty values, but not throw + expect(result).toHaveProperty("missing_ref"); + expect(result.missing_ref).toBeDefined(); + // Should be an object, but all values undefined except loopOver/parentLoop + expect(result.missing_ref.loopOver).toBeUndefined(); + }); +}); diff --git a/ui-next/src/pages/execution/state/executionMapper.ts b/ui-next/src/pages/execution/state/executionMapper.ts new file mode 100644 index 0000000000..8db48ca6dd --- /dev/null +++ b/ui-next/src/pages/execution/state/executionMapper.ts @@ -0,0 +1,438 @@ +import { MAX_EXPAND_TASKS } from "components/flow/nodes/constants"; +import _curry from "lodash/curry"; +import _findLast from "lodash/findLast"; +import _first from "lodash/first"; +import _path from "lodash/fp/path"; +import _identity from "lodash/identity"; +import _mapValues from "lodash/mapValues"; +import _nth from "lodash/nth"; +import _omit from "lodash/omit"; +import _pick from "lodash/pick"; +import _xor from "lodash/xor"; +import { + DoWhileSelection, + ExecutedData, + Execution, + ExecutionTask, + TaskDef, + TaskStatus, + TaskType, +} from "types"; +import { + DynamicForkRelations, + StatusMap, + TypeStatusMap, +} from "./StatusMapTypes"; +import { TaskDefExecutionContext, WorkflowDefExecutionContext } from "./types"; + +export const relatedNamesToTaskDef = ( + names: string[], + executionTasks: ExecutionTask[], + parentTaskReferenceName: string, +) => { + const relationTasks = names.map((tn) => + executionTasks.find((t) => t.workflowTask.taskReferenceName === tn), + ); + const taskWithSiblings = names.reduce( + (tnAcc, tn) => ({ + ...tnAcc, + [tn]: { + siblings: relationTasks, + parentTaskReferenceName, + }, + }), + {}, + ); + return taskWithSiblings; +}; + +type StatusMapTupleAcumulator = [ + StatusMap, + Record, +]; +export const executionTasksToStatusMap = (executionTasks: ExecutionTask[]) => { + const [statusMap] = executionTasks.reduce( + ( + [acc, related]: StatusMapTupleAcumulator, + task: ExecutionTask, + idx: number, + ): StatusMapTupleAcumulator => { + const loopOver = acc[task.workflowTask.taskReferenceName]?.loopOver || []; + let newRelated = related; + if (task.workflowTask.type === TaskType.FORK_JOIN_DYNAMIC) { + newRelated = { + ...related, + ...relatedNamesToTaskDef( + task.inputData?.forkedTasks || [], + executionTasks, + task.workflowTask.taskReferenceName, + ), + }; + } + + const targetSlice = executionTasks?.slice(0, idx); + + return [ + { + ...acc, + [task.workflowTask.taskReferenceName]: { + ...task, + loopOver: loopOver.concat(task), + related: related[task.workflowTask.taskReferenceName], + parentLoop: task.loopOverTask + ? _findLast(targetSlice, (t) => t.taskType === TaskType.DO_WHILE) + : undefined, + }, + } as Record, + newRelated, + ]; + }, + [{}, {}], + ); + return statusMap; +}; + +const extractFirstTaskReferenceName = (tasks: TaskDef[]) => { + const firstTask = _first(tasks); + return firstTask == null ? "no_task" : firstTask.taskReferenceName; +}; + +const entireCollapsedStatus = ( + executionTask: TypeStatusMap, + firstTaskExecutionDataRaw: Pick< + TypeStatusMap, + "status" | "executed" | "loopOver" + >, + statusMap: StatusMap, + returnStatusArray = false, +) => { + const forkedTaskDefs: TaskDef[] = + _path("inputData.forkedTaskDefs", executionTask) ?? []; + const collapsedTaskRefNames = forkedTaskDefs + ? forkedTaskDefs.map((item) => item.taskReferenceName) + : []; + const collapsedTasksStatus = + collapsedTaskRefNames.map((item) => statusMap[item]?.status) ?? []; + if (returnStatusArray) { + return collapsedTasksStatus; + } + if (collapsedTasksStatus.includes(TaskStatus.FAILED)) { + return TaskStatus.FAILED; + } else if (collapsedTasksStatus.includes(TaskStatus.TIMED_OUT)) { + return TaskStatus.TIMED_OUT; + } else if (collapsedTasksStatus.includes(TaskStatus.SKIPPED)) { + return TaskStatus.SKIPPED; + } else if (collapsedTasksStatus.includes(TaskStatus.CANCELED)) { + return TaskStatus.CANCELED; + } else { + return firstTaskExecutionDataRaw.status; + } +}; + +export const taskStatusUpdater = ( + tasks: TaskDef[] = [], + statusMap: StatusMap, + expandDynamic: string[], +): TaskDefExecutionContext[] => { + return tasks.map((task) => { + const { type, taskReferenceName } = task; + const executionTask = statusMap[taskReferenceName]; + + const executionDataRaw = _pick( + executionTask || { + status: TaskStatus.PENDING, + executed: false, + loopOver: [], + }, + ["status", "executed", "loopOver"], + ); + const executionData: ExecutedData = { + status: executionDataRaw.status as TaskStatus, + executed: executionDataRaw.executed, + attempts: executionDataRaw.loopOver.length, + outputData: executionTask?.outputData, + parentLoop: executionTask?.parentLoop, + }; + + if (type === TaskType.FORK_JOIN) { + const forkTasks: Array = ( + task.forkTasks || [] + ).map((taa) => taskStatusUpdater(taa, statusMap, expandDynamic)); + return { + ...task, + forkTasks, + executionData, + } as TaskDefExecutionContext; + } else if (type === TaskType.DECISION || type === TaskType.SWITCH) { + const decisionCases: Record = + _mapValues(task.decisionCases, (decisionTasks: TaskDef[]) => + taskStatusUpdater(decisionTasks, statusMap, expandDynamic), + ); + return { + ...task, + decisionCases, + defaultCase: taskStatusUpdater( + task.defaultCase!, + statusMap, + expandDynamic, + ), + executionData, + } as TaskDefExecutionContext; + } else if (type === TaskType.DO_WHILE) { + return { + ...task, + loopOver: taskStatusUpdater(task.loopOver!, statusMap, expandDynamic), + executionData, + } as TaskDefExecutionContext; + } else if ( + type === TaskType.FORK_JOIN_DYNAMIC && + executionData.status !== TaskStatus.PENDING + ) { + const doesNotRequireCollapse = + (executionTask!.inputData?.forkedTaskDefs || []).length < + MAX_EXPAND_TASKS; + + if (expandDynamic.includes(taskReferenceName) || doesNotRequireCollapse) { + const forkTasks: Array = taskStatusUpdater( + executionTask!.inputData!.forkedTaskDefs, + statusMap, + expandDynamic, + ).map((t: TaskDefExecutionContext) => [t]); + return { + ...task, + forkTasks, + joinOn: executionTask?.inputData?.forkedTasks, + executionData: { + ...executionData, + ...(doesNotRequireCollapse ? {} : { collapsed: false }), + }, + } as TaskDefExecutionContext; + } + + const firstTaskReferenceName = extractFirstTaskReferenceName( + executionTask!.inputData!.forkedTaskDefs, + ); + + const firstTaskExecutionDataRaw = _pick( + statusMap[firstTaskReferenceName] || { + status: TaskStatus.PENDING, + executed: false, + }, + ["status", "executed", "loopOver"], + ); + + const firstTaskExecutionData = { + status: entireCollapsedStatus( + executionTask, + firstTaskExecutionDataRaw, + statusMap, + ), + executed: firstTaskExecutionDataRaw?.executed, + attempts: firstTaskExecutionDataRaw?.loopOver?.length, + }; + return { + ...task, + forkTasks: [ + [ + { + type: "FORK_JOIN_COLLAPSED" as TaskType, + taskReferenceName: firstTaskReferenceName, + executionData: { + ...firstTaskExecutionData, + collapsedTasks: executionTask?.inputData?.forkedTaskDefs, + parentTaskReferenceName: task.taskReferenceName, + collapsedTasksStatus: entireCollapsedStatus( + executionTask, + firstTaskExecutionDataRaw, + statusMap, + true, + ), + }, + }, + ], + ], + executionData: { + ...executionData, + collapsed: true, + }, + } as TaskDefExecutionContext; + } else if (type === TaskType.SIMPLE) { + return { + ...task, + executionData: { + ...executionData, + domain: executionTask?.domain, + }, + } as TaskDefExecutionContext; + } else if (type === TaskType.TASK_SUMMARY) { + return { + ...task, + executionData: { + ...executionData, + summary: executionTask?.outputData?.summary, + }, + } as TaskDefExecutionContext; + } + return { + ...task, + executionData, + } as TaskDefExecutionContext; + }); +}; + +const getTasksOfCurrentIteration = ( + selectedIteration: number, + selectedDowhileRef?: string, + statusMap: StatusMap = {}, +) => { + let result = []; + for (const key in statusMap) { + if ( + Object.prototype.hasOwnProperty.call(statusMap, key) && + statusMap[key]["iteration"] === selectedIteration + ) { + result.push({ taskRefName: key, taskType: statusMap[key].taskType }); + } + } + if (selectedDowhileRef) { + result = result.filter((item) => item.taskType !== TaskType.DO_WHILE); + } + return result?.map((item) => item.taskRefName); +}; + +export const doWhileSelectionForStatusMap = ( + doWhileSelection?: DoWhileSelection[], + statusMap?: StatusMap, +) => { + const [keysToOmmit, finalMapArray] = doWhileSelection + ? doWhileSelection.reduce< + [keysToOmmit: string[], finalMapArray: Omit[]] + >( + (acc, item) => { + const doWhileTask = statusMap![item?.doWhileTaskReferenceName]; + + const { outputData } = doWhileTask; + const taskReferencesForIteration = Object.keys( + _path(item?.selectedIteration, outputData) || {}, + ); + + const allReferences = Array.from( + new Set( + Object.values(outputData ?? {}).flatMap((obj) => + Object.keys(obj ?? {}), + ), + ), + ); + + // if tasksReferencesForIteration is there, use it. if not, we have get the tasks of currentIteration from the statusMap using getTasksOfCurrentIteration() + const updatedTaskReferenceForIteration = + taskReferencesForIteration && taskReferencesForIteration.length > 0 + ? taskReferencesForIteration + : getTasksOfCurrentIteration( + item?.selectedIteration, + item?.doWhileTaskReferenceName, + statusMap, + ); + + const updatedStatusMap = updatedTaskReferenceForIteration.reduce( + (acc: any, taskReferenceName: string) => { + const currentAssociatedTask = statusMap![taskReferenceName]; + + const loopOverForAssociatedTask = currentAssociatedTask?.loopOver; + const maybeParentLoop = currentAssociatedTask?.parentLoop; + const taskForIterationNumber = loopOverForAssociatedTask?.find( + (task: Partial) => + task.iteration === item?.selectedIteration, + ); + + return { + ...acc, + [taskReferenceName]: { + ...taskForIterationNumber, + loopOver: loopOverForAssociatedTask, + parentLoop: maybeParentLoop, + }, + }; + }, + {}, + ); + const keysToRemove = _xor( + updatedTaskReferenceForIteration, + allReferences, + ); + const latestMap = _omit(updatedStatusMap, keysToRemove); + + return [ + [...acc[0], ...keysToRemove], + [...acc[1], latestMap], + ]; + }, + + [[], []], + ) + : [[], []]; + const latestMap = _omit(statusMap, keysToOmmit); + // Spreads finalMap as arguments to the Object.assign call + return Object.assign({}, latestMap, ...(finalMapArray ?? [])); +}; + +const updateStatusMapWithLatestRetryCount = ( + statusMap: StatusMap, + selectedTask: ExecutionTask, +) => { + const { retryCount, referenceTaskName } = selectedTask; + const currentTaskLoopOverFromMap = + statusMap[referenceTaskName]?.loopOver ?? []; + + const currentTaskWithUpdatedRetryCount = + _nth( + currentTaskLoopOverFromMap?.filter( + (item) => item.retryCount === retryCount, + ), + 0, + ) ?? {}; + + const updatedStatusMap = { + ...statusMap, + [referenceTaskName]: { + ...currentTaskWithUpdatedRetryCount, + loopOver: currentTaskLoopOverFromMap, + }, + }; + return updatedStatusMap; +}; + +export const executionToWorkflowDef = ( + execution: Execution, + expandDynamic = [], + doWhileSelection?: DoWhileSelection[], + selectedTask?: ExecutionTask, +): [WorkflowDefExecutionContext, StatusMap] => { + const doWhileModifier = + doWhileSelection == null + ? _identity + : _curry(doWhileSelectionForStatusMap)(doWhileSelection); + + const basicExecutionMap = executionTasksToStatusMap(execution.tasks); + + const updatedExecutionMapWithLatestRetryCount = + selectedTask && selectedTask?.taskType === TaskType.DO_WHILE + ? updateStatusMapWithLatestRetryCount(basicExecutionMap, selectedTask) + : basicExecutionMap; + + const taskExecutionMap = doWhileModifier( + updatedExecutionMapWithLatestRetryCount, + ); + + return [ + { + ...execution.workflowDefinition, + tasks: taskStatusUpdater( + execution.workflowDefinition.tasks, + taskExecutionMap, + expandDynamic, + ), + }, + taskExecutionMap, + ]; +}; diff --git a/ui-next/src/pages/execution/state/guards.ts b/ui-next/src/pages/execution/state/guards.ts new file mode 100644 index 0000000000..c7ac6002bb --- /dev/null +++ b/ui-next/src/pages/execution/state/guards.ts @@ -0,0 +1,58 @@ +import isNil from "lodash/isNil"; +import { + COUNT_DOWN_TYPE, + ExecutionMachineContext, + ExecutionTabs, +} from "./types"; +import { HttpStatusCode } from "utils/constants/httpStatusCode"; +import { DoneInvokeEvent } from "xstate"; + +const WOKFLOW_TERMINATED_STATUS = [ + "COMPLETED", + "FAILED", + "TIMED_OUT", + "TERMINATED", +]; + +export const canWorkflowChangeState = (context: ExecutionMachineContext) => + !WOKFLOW_TERMINATED_STATUS.includes(context?.execution?.status || "") && + !isNil(context.execution); + +export const isExecutionTerminated = (context: ExecutionMachineContext) => + context?.execution?.status === "TERMINATED"; +export const isExecutionCompleted = (context: ExecutionMachineContext) => + context?.execution?.status === "COMPLETED"; +export const isExecutionFailed = (context: ExecutionMachineContext) => + context?.execution?.status === "FAILED"; +export const isExecutionTimedOut = (context: ExecutionMachineContext) => + context?.execution?.status === "TIMED_OUT"; +export const isExecutionPaused = (context: ExecutionMachineContext) => + context?.execution?.status === "PAUSED"; + +export const isTaskListTab = ({ currentTab }: ExecutionMachineContext) => + currentTab === ExecutionTabs.TASK_LIST_TAB; + +export const isTimeLineTab = ({ currentTab }: ExecutionMachineContext) => + currentTab === ExecutionTabs.TIMELINE_TAB; + +export const isTimeWorkflowInputOutputTab = ({ + currentTab, +}: ExecutionMachineContext) => + currentTab === ExecutionTabs.WORKFLOW_INPUT_OUTPUT_TAB; + +export const isJsonTab = ({ currentTab }: ExecutionMachineContext) => + currentTab === ExecutionTabs.JSON_TAB; + +export const isSummaryTab = ({ currentTab }: ExecutionMachineContext) => + currentTab === ExecutionTabs.SUMMARY_TAB; + +export const isInfinityCountdown = (context: ExecutionMachineContext) => + context?.countdownType === COUNT_DOWN_TYPE.INFINITE; + +export const isUseGlobalMessage = ( + __: ExecutionMachineContext, + event: DoneInvokeEvent<{ + originalError: Response; + errorDetails: { message: string }; + }>, +) => event?.data?.originalError?.status === HttpStatusCode.Forbidden; diff --git a/ui-next/src/pages/execution/state/hook.ts b/ui-next/src/pages/execution/state/hook.ts new file mode 100644 index 0000000000..733b043465 --- /dev/null +++ b/ui-next/src/pages/execution/state/hook.ts @@ -0,0 +1,338 @@ +import { useInterpret, useSelector } from "@xstate/react"; +import { FlowActionTypes, SelectNodeEvent } from "components/flow/state"; +import { selectNodes } from "components/flow/state/selectors"; +import { MessageContext } from "components/v1/layout/MessageContext"; +import _isEmpty from "lodash/isEmpty"; +import { useContext, useEffect } from "react"; +import { useNavigate, useParams } from "react-router"; +import { useQueryState } from "react-router-use-location-state"; +import { NodeData } from "reaflow"; +import { ExecutionTask, TaskStatus } from "types"; +import { + RUN_WORKFLOW_URL, + SCHEDULER_DEFINITION_URL, +} from "utils/constants/route"; +import { featureFlags, FEATURES } from "utils/flags"; +import { useAuthHeaders, useCurrentUserInfo } from "utils/query"; + +import { taskWithLatestIteration } from "../helpers"; +import { + RightPanelContextEventTypes, + SetSelectedTaskEvent, +} from "../RightPanel"; +import { executionMachine } from "./machine"; +import { + ExecutionActionTypes, + ExecutionTabs, + UpdateQueryParamEvent, +} from "./types"; + +const isPlayground = featureFlags.isEnabled(FEATURES.PLAYGROUND); +export const useExecutionMachine = () => { + const authHeaders = useAuthHeaders(); + const { setMessage } = useContext(MessageContext); + const navigate = useNavigate(); + const { data: currentUserInfo } = useCurrentUserInfo(); + + const [tabIndex, setTabIndex] = useQueryState("tab", ""); + const [taskReferenceName, handleTaskReferenceName] = useQueryState( + "taskReferenceName", + "", + ); + const [taskId, handleTaskId] = useQueryState("taskId", ""); + + const service = useInterpret(executionMachine, { + ...(process.env.NODE_ENV === "development" ? { devTools: true } : {}), + context: { + duration: isPlayground ? 10 : 30, + authHeaders, + currentUserInfo, + selectedTaskReferenceName: taskReferenceName, + selectedTaskId: taskId, + }, + actions: { + setErrorMessage: (context, event: any) => { + setMessage({ + severity: "error", + text: event?.data?.errorDetails?.message, + }); + }, + updateQueryParam: (_context, data: UpdateQueryParamEvent) => { + handleTaskReferenceName(data?.taskReferenceName || ""); + }, + setQueryParam: ({ execution }, { node }: SelectNodeEvent) => { + const taskRefName = node?.data?.task?.taskReferenceName; + handleTaskReferenceName(taskRefName || ""); + const executionStatus = node?.data?.task?.executionData?.status; + if (executionStatus !== TaskStatus.PENDING) { + const selectedTask = taskWithLatestIteration( + execution?.tasks, + taskRefName, + ); + handleTaskId(selectedTask?.taskId || ""); + } + }, + setTaskIdQueryParam: (_context, data: SetSelectedTaskEvent) => { + if (data?.selectedTask?.taskId) { + handleTaskId(data?.selectedTask?.taskId); + } + }, + clearQueryParams: (_context) => { + handleTaskId(""); + handleTaskReferenceName(""); + }, + // Badge count is now updated by HumanTasksPoller at its next poll cycle. + notifyOnHumanTask: () => {}, + }, + }); + const params = useParams<{ id: string }>(); + const executionId = params?.id; + + const execution = useSelector(service, (state) => state.context.execution); + const executionTasks = useSelector( + service, + (state) => state.context.execution?.tasks, + ); + + const flowChildActor = useSelector( + service, + (state) => state.context.flowChild, + ); + + const nodes = useSelector(flowChildActor!, selectNodes); + const maybeError = useSelector(service, (state) => state.context.error); + const maybeMessage = useSelector(service, (state) => state.context.message); + + const send = service.send; + useEffect(() => { + if (executionId) { + send({ + type: ExecutionActionTypes.UPDATE_EXECUTION, + executionId, + }); + } + }, [executionId, send]); + + const changeExecutionTab = (tab: ExecutionTabs) => { + setTabIndex(tab); + }; + + const openedTab = useSelector(service, (state) => state.context.currentTab); + + const isReady = useSelector(service, (state) => state.matches("init")); + + const isNoAccess = useSelector(service, (state) => state.matches("noAccess")); + + useEffect(() => { + if (!_isEmpty(tabIndex) && openedTab !== tabIndex && isReady) { + send({ + type: ExecutionActionTypes.CHANGE_EXECUTION_TAB, + tab: tabIndex as ExecutionTabs, + }); + } + }, [tabIndex, openedTab, send, isReady]); + + const refetch = () => send({ type: ExecutionActionTypes.REFETCH }); + const closeRightPanel = () => { + send({ + type: ExecutionActionTypes.CLOSE_RIGHT_PANEL, + }); + }; + + const selectTask = (taskSel: { ref?: string; taskId?: string }) => { + const maybeSelectedTask = executionTasks?.find( + (task: ExecutionTask) => + task.taskId === taskSel.taskId || + task.referenceTaskName === taskSel.ref, + ); + if (maybeSelectedTask) { + send({ + type: RightPanelContextEventTypes.SET_SELECTED_TASK, + selectedTask: maybeSelectedTask, + }); + } else { + closeRightPanel(); + } + }; + + const expandDynamic = (taskReferenceName: string) => + send({ type: ExecutionActionTypes.EXPAND_DYNAMIC_TASK, taskReferenceName }); + + const collapseDynamic = (taskReferenceName: string) => + send({ + type: ExecutionActionTypes.COLLAPSE_DYNAMIC_TASK, + taskReferenceName, + }); + + const clearError = () => + send({ + type: ExecutionActionTypes.CLEAR_ERROR, + }); + + const resumeExecution = () => { + send({ + type: ExecutionActionTypes.RESUME_EXECUTION, + }); + }; + + const pauseExecution = () => { + send({ + type: ExecutionActionTypes.PAUSE_EXECUTION, + }); + }; + + const terminateExecution = () => { + send({ + type: ExecutionActionTypes.TERMINATE_EXECUTION, + }); + }; + + const rerunExecutionWithLatestDefinitions = () => { + navigate(RUN_WORKFLOW_URL, { state: { execution } }); + }; + + const createSheduleWithLatestDefinitions = () => { + navigate(SCHEDULER_DEFINITION_URL.NEW, { state: { execution } }); + }; + + const restartExecutionWithLatestDefinitions = () => { + send({ + type: ExecutionActionTypes.RESTART_EXECUTION, + options: { + useLatestDefinitions: "true", + }, + }); + }; + + const restartExecutionWithCurrentDefinitions = () => { + send({ + type: ExecutionActionTypes.RESTART_EXECUTION, + options: {}, + }); + }; + + const retryExcutionFromFailed = () => { + send({ + type: ExecutionActionTypes.RETRY_EXECUTION, + options: { + resumeSubworkflowTasks: "false", + }, + }); + }; + + const retryResumeSubworkflow = () => { + send({ + type: ExecutionActionTypes.RETRY_EXECUTION, + options: { + resumeSubworkflowTasks: "true", + }, + }); + }; + const updateDuration = (duration: number) => { + send({ + type: ExecutionActionTypes.UPDATE_DURATION, + duration, + }); + }; + + const handleUpdateVariables = (data: string) => { + send({ + type: ExecutionActionTypes.UPDATE_VARIABLES, + data: data, + }); + }; + + const selectNode = (node: NodeData) => { + if (flowChildActor) { + flowChildActor.send({ + type: FlowActionTypes.SELECT_NODE_EVT, + node, + }); + } + }; + + // const selectTaskWithTaskRef = (node: NodeData, exactTaskRef: string) => { + // if (flowChildActor) { + // flowChildActor.send({ + // type: FlowActionTypes.SELECT_TASK_WITH_TASK_REF, + // node, + // exactTaskRef, + // }); + // } + // }; + + const executionStatusMap = useSelector( + service, + (state) => state.context.executionStatusMap, + ); + + const taskListActor = useSelector( + service, + (state) => state.children.taskListMachine, + ); + + const rightPanelActor = useSelector( + service, + (state) => state.children?.rightPanelMachine, + ); + + const doWhileSelection = useSelector( + service, + (state) => state.context.doWhileSelection, + ); + + const isAssistantPanelOpen = useSelector( + service, + (state) => state.context.isAssistantPanelOpen, + ); + + const toggleAssistantPanel = () => { + send({ + type: ExecutionActionTypes.TOGGLE_ASSISTANT_PANEL, + }); + }; + + const stateSelectors = { + flowActor: flowChildActor, + countdownActor: service?.children?.get("countdownMachine"), + execution, + executionId, + isReady, + executionStatusMap, + maybeError, + maybeMessage, + openedTab, + taskListActor, + rightPanelActor, + isNoAccess, + doWhileSelection, + nodes, + isAssistantPanelOpen, + }; + + return [ + { + refetch, + selectTask, + expandDynamic, + collapseDynamic, + clearError, + rerunExecutionWithLatestDefinitions, + createSheduleWithLatestDefinitions, + restartExecutionWithLatestDefinitions, + restartExecutionWithCurrentDefinitions, + retryExcutionFromFailed, + resumeExecution, + terminateExecution, + pauseExecution, + retryResumeSubworkflow, + changeExecutionTab, + updateDuration, + closeRightPanel, + handleUpdateVariables, + selectNode, + toggleAssistantPanel, + }, + stateSelectors, + ] as const; +}; diff --git a/ui-next/src/pages/execution/state/index.ts b/ui-next/src/pages/execution/state/index.ts new file mode 100644 index 0000000000..01417f405f --- /dev/null +++ b/ui-next/src/pages/execution/state/index.ts @@ -0,0 +1,2 @@ +export * from "./FlowExecutionContext"; +export * from "./types"; diff --git a/ui-next/src/pages/execution/state/machine.ts b/ui-next/src/pages/execution/state/machine.ts new file mode 100644 index 0000000000..9780faf8ef --- /dev/null +++ b/ui-next/src/pages/execution/state/machine.ts @@ -0,0 +1,546 @@ +import { createMachine } from "xstate"; +import * as actions from "./actions"; +import * as guards from "./guards"; +import * as services from "./services"; +import { FlowActionTypes } from "components/flow/state/types"; +import { + ExecutionActionTypes, + ExecutionMachineEvents, + ExecutionMachineContext, + ExecutionTabs, + COUNT_DOWN_TYPE, +} from "./types"; +import { taskListMachine } from "../TaskList"; +import { + RightPanelContextEventTypes, + SetSelectedTaskEvent, + rightPanelMachine, +} from "../RightPanel"; +import { SUMMARY_TAB } from "./constants"; + +import { countdownMachine } from "./countdownMachine"; + +export const executionMachine = createMachine< + ExecutionMachineContext, + ExecutionMachineEvents +>( + { + id: "executionDefintionMachine", + initial: "idle", + predictableActionArguments: true, + context: { + execution: undefined, + executionId: undefined, + flowChild: undefined, + error: undefined, + expandedDynamic: [], + authHeaders: undefined, + currentTab: ExecutionTabs.DIAGRAM_TAB, + duration: 30, + countdownType: COUNT_DOWN_TYPE.INFINITE, + isDisabledCountdown: false, + executionStatusMap: undefined, + isAssistantPanelOpen: false, + }, + on: { + [ExecutionActionTypes.UPDATE_EXECUTION]: { + actions: ["persistExecutionId"], + target: "fetchForExecution", + }, + [ExecutionActionTypes.REPORT_FLOW_ERROR]: { + actions: ["persistFlowError"], + }, + }, + states: { + idle: { + entry: "instanciateFlow", + }, + fetchForExecution: { + // Initial fetch by url + invoke: { + id: "executionFetcher", + src: "fetchExecution", + onDone: { + actions: ["updateExecution", "notifyOnHumanTask"], + target: "init", + }, + onError: [ + { + cond: "isUseGlobalMessage", + actions: "setErrorMessage", + target: "noAccess", + }, + { + actions: ["logError", "assignError"], + target: "init", + }, + ], + }, + }, + noAccess: { + type: "final", + }, + init: { + type: "parallel", + states: { + rightPanel: { + initial: "closed", + states: { + opened: { + on: { + [FlowActionTypes.SELECT_NODE_EVT]: { + actions: ["nodeToTaskSelectionToPanel", "setQueryParam"], + }, + [FlowActionTypes.SELECT_TASK_WITH_TASK_REF]: { + actions: ["taskToTaskSelectionToPanel"], + }, + [ExecutionActionTypes.UPDATE_TASKID_IN_URL]: { + actions: ["setTaskIdQueryParam"], + }, + [RightPanelContextEventTypes.SET_SELECTED_TASK]: { + actions: ["forwardSelectionToPanel"], + }, + [ExecutionActionTypes.FETCH_FOR_LOGS]: { + actions: ["forwardSelectionToPanel"], + }, + [ExecutionActionTypes.CLOSE_RIGHT_PANEL]: { + target: "closed", + }, + [ExecutionActionTypes.EXECUTION_UPDATED]: { + actions: ["sendUpdatedExecution"], + }, + [ExecutionActionTypes.SET_DO_WHILE_ITERATION]: { + actions: [ + "persistDoWhileIteration", + "updateWorkflowDefinition", + "notifyFlowUpdates", + ], + }, + [ExecutionActionTypes.UPDATE_SELECTED_TASK]: { + actions: [ + "updateSelectedTask", + "updateExecutionMap", + "notifyFlowUpdates", + ], + }, + [ExecutionActionTypes.UPDATE_QUERY_PARAM]: { + actions: ["updateQueryParam"], + }, + [ExecutionActionTypes.TOGGLE_ASSISTANT_PANEL]: { + actions: ["toggleAssistantPanel"], + target: "closed", + }, + }, + invoke: { + src: rightPanelMachine, + id: "rightPanelMachine", + data: { + selectedTask: ( + __context: ExecutionMachineContext, + event: SetSelectedTaskEvent, + ) => { + return event.selectedTask; + }, + authHeaders: ({ authHeaders }: ExecutionMachineContext) => + authHeaders, + executionId: ({ executionId }: ExecutionMachineContext) => + executionId, + executionStatusMap: ({ + executionStatusMap, + }: ExecutionMachineContext) => executionStatusMap, + currentTab: SUMMARY_TAB, + }, + onDone: { + actions: [ + "cleanSelection", + "selectNodeInFlow", + "gtagEventLogger", + "clearQueryParams", + ], + target: "closed", + }, + }, + }, + closed: { + on: { + [RightPanelContextEventTypes.SET_SELECTED_TASK]: { + actions: ["closeAssistantPanel"], + target: "opened", + }, + [FlowActionTypes.SELECT_NODE_EVT]: { + actions: [ + "nodeToTaskSelectionToPanel", + "setQueryParam", + "closeAssistantPanel", + ], + target: "opened", + }, + [FlowActionTypes.SELECT_TASK_WITH_TASK_REF]: { + actions: [ + "taskToTaskSelectionToPanel", + "closeAssistantPanel", + ], + target: "opened", + }, + [ExecutionActionTypes.UPDATE_QUERY_PARAM]: { + actions: ["updateQueryParam"], + }, + [ExecutionActionTypes.TOGGLE_ASSISTANT_PANEL]: { + actions: ["toggleAssistantPanel"], + }, + }, + }, + }, + }, + detailSelection: { + initial: "initDiagram", + on: { + [ExecutionActionTypes.CHANGE_EXECUTION_TAB]: { + target: ".addressChangeTab", + actions: ["persistCurrentTab", "gtagEventLogger"], + }, + }, + states: { + initDiagram: { + always: [ + { + cond: (ctx) => + !!ctx.selectedTaskReferenceName || !!ctx.selectedTaskId, + actions: ["notifyFlowUpdates", "delayedNodeSelection"], + target: "diagram", + }, + { + target: "diagram", + }, + ], + }, + addressChangeTab: { + always: [ + { + target: "taskList", + cond: "isTaskListTab", + }, + { + target: "timeLine", + cond: "isTimeLineTab", + }, + { + target: "workflowInputOutput", + cond: "isTimeWorkflowInputOutputTab", + }, + { + target: "json", + cond: "isJsonTab", + }, + { + target: "summary", + cond: "isSummaryTab", + }, + { target: "diagram" }, + ], + }, + diagram: { + entry: "notifyFlowUpdates", + on: { + [ExecutionActionTypes.EXPAND_DYNAMIC_TASK]: { + actions: [ + "addToExpandedDynamic", + "updateWorkflowDefinition", + "notifyFlowUpdates", + "startRenderingGtag", + ], + }, + [ExecutionActionTypes.COLLAPSE_DYNAMIC_TASK]: { + actions: [ + "removeFromExpandedDynamic", + "updateWorkflowDefinition", + "notifyFlowUpdates", + "startRenderingGtag", + ], + }, + }, + }, + taskList: { + invoke: { + src: taskListMachine, + id: "taskListMachine", + data: { + authHeaders: ({ authHeaders }: ExecutionMachineContext) => + authHeaders, + executionId: ({ executionId }: ExecutionMachineContext) => + executionId, + startIndex: 0, + rowsPerPage: 20, + }, + onDone: {}, + }, + }, + timeLine: {}, + summary: {}, + workflowInputOutput: {}, + json: {}, + }, + }, + executionActions: { + initial: "determineExecutionCurrentState", + on: { + [ExecutionActionTypes.REFETCH]: { + target: ".fetchForExecution", + actions: ["gtagEventLogger"], + }, + [ExecutionActionTypes.CLEAR_ERROR]: { + actions: ["clearError", "gtagEventLogger"], + }, + [ExecutionActionTypes.UPDATE_DURATION]: { + actions: ["updateExecutionDuration", "gtagEventLogger"], + }, + }, + states: { + fetchForExecution: { + invoke: { + id: "executionFetcher", + src: "fetchExecution", + onDone: { + actions: [ + "updateExecution", + "notifyFlowUpdates", + "startRenderingGtag", + "raiseExecutionUpdated", + "notifyOnHumanTask", + ], + target: "determineExecutionCurrentState", + }, + onError: { + actions: ["logError", "assignError", "gtagErrorLogger"], + target: "determineExecutionCurrentState", + }, + }, + }, + delayFetchForExecution: { + after: { + 1000: { + target: "fetchForExecution", + }, + }, + }, + determineExecutionCurrentState: { + // states should rely on the EXECUTION status + always: [ + { + target: "finishedExecution.terminated", + cond: "isExecutionTerminated", + }, + { + target: "finishedExecution.failed", + cond: "isExecutionFailed", + }, + { + target: "finishedExecution.timedOut", + cond: "isExecutionTimedOut", + }, + { + target: "finishedExecution.paused", + cond: "isExecutionPaused", + }, + { + target: "finishedExecution.completed", + cond: "isExecutionCompleted", + }, + { + target: "runningExecution", + }, + ], + }, + terminateExecution: { + invoke: { + id: "terminateExecutionService", + src: "terminateExecution", + onDone: { + target: "delayFetchForExecution", + }, + onError: { + actions: ["logError", "assignError", "gtagErrorLogger"], + target: "fetchForExecution", + }, + }, + }, + pauseExecution: { + invoke: { + id: "pauseExecutionService", + src: "pauseExecution", + onDone: { + target: "delayFetchForExecution", + }, + onError: { + actions: ["logError", "assignError", "gtagErrorLogger"], + target: "fetchForExecution", + }, + }, + }, + resumeExecution: { + invoke: { + id: "resumeExecutionService", + src: "resumeExecution", + onDone: { + target: "delayFetchForExecution", + }, + onError: { + actions: ["logError", "assignError", "gtagErrorLogger"], + target: "fetchForExecution", + }, + }, + }, + restartExecution: { + invoke: { + id: "restartExecutionService", + src: "restartExecution", + onDone: { + target: "delayFetchForExecution", + }, + onError: { + actions: ["logError", "assignError", "gtagErrorLogger"], + target: "fetchForExecution", + }, + }, + }, + retryExecution: { + invoke: { + id: "retryExecutionService", + src: "retryExecution", + onDone: { + target: "delayFetchForExecution", + }, + onError: { + actions: ["logError", "assignError", "gtagErrorLogger"], + target: "fetchForExecution", + }, + }, + }, + runningExecution: { + invoke: { + id: "countdownMachine", + src: countdownMachine, + data: { + duration: (context: ExecutionMachineContext) => + context.duration, + elapsed: 0, + executionStatus: "", + countdownType: (context: ExecutionMachineContext) => + context.countdownType, + isDisabled: (context: ExecutionMachineContext) => + context.isDisabledCountdown, + }, + onDone: { + actions: ["fetchForLogs"], + target: "fetchForExecution", + }, + }, + on: { + [ExecutionActionTypes.TERMINATE_EXECUTION]: { + target: "terminateExecution", + actions: ["gtagEventLogger"], + }, + [ExecutionActionTypes.PAUSE_EXECUTION]: { + target: "pauseExecution", + actions: ["gtagEventLogger"], + }, + [ExecutionActionTypes.UPDATE_VARIABLES]: { + target: "updateVariablesOfExecution", + }, + }, + }, + updateVariablesOfExecution: { + invoke: { + id: "updateVariablesService", + src: "updateVariables", + onDone: { + actions: ["persistSuccessUpdateVariablesMessage"], + target: "delayFetchForExecution", + }, + onError: { + actions: ["logError", "assignError", "gtagErrorLogger"], + }, + }, + }, + finishedExecution: { + initial: "completed", + states: { + completed: { + on: { + [ExecutionActionTypes.RESTART_EXECUTION]: { + target: + "#executionDefintionMachine.init.executionActions.restartExecution", + }, + }, + }, + terminated: { + on: { + [ExecutionActionTypes.RESTART_EXECUTION]: { + target: + "#executionDefintionMachine.init.executionActions.restartExecution", + }, + [ExecutionActionTypes.RETRY_EXECUTION]: { + target: + "#executionDefintionMachine.init.executionActions.retryExecution", + }, + }, + }, + failed: { + on: { + [ExecutionActionTypes.RESTART_EXECUTION]: { + target: + "#executionDefintionMachine.init.executionActions.restartExecution", + }, + [ExecutionActionTypes.RETRY_EXECUTION]: { + target: + "#executionDefintionMachine.init.executionActions.retryExecution", + }, + [ExecutionActionTypes.TERMINATE_EXECUTION]: { + target: + "#executionDefintionMachine.init.executionActions.terminateExecution", + }, + }, + }, + timedOut: { + on: { + [ExecutionActionTypes.RESTART_EXECUTION]: { + target: + "#executionDefintionMachine.init.executionActions.restartExecution", + }, + [ExecutionActionTypes.RETRY_EXECUTION]: { + target: + "#executionDefintionMachine.init.executionActions.retryExecution", + }, + + [ExecutionActionTypes.TERMINATE_EXECUTION]: { + target: + "#executionDefintionMachine.init.executionActions.terminateExecution", + }, + }, + }, + paused: { + on: { + [ExecutionActionTypes.RESUME_EXECUTION]: { + target: + "#executionDefintionMachine.init.executionActions.resumeExecution", + }, + [ExecutionActionTypes.TERMINATE_EXECUTION]: { + target: + "#executionDefintionMachine.init.executionActions.terminateExecution", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + actions: actions as any, + guards: guards as any, + services: services as any, + }, +); diff --git a/ui-next/src/pages/execution/state/sampleExecutions.js b/ui-next/src/pages/execution/state/sampleExecutions.js new file mode 100644 index 0000000000..b564bf5e1a --- /dev/null +++ b/ui-next/src/pages/execution/state/sampleExecutions.js @@ -0,0 +1,51383 @@ +export const sampleExecution = { + ownerApp: "", + createTime: 1648555003451, + status: "FAILED", + endTime: 1648585762718, + workflowId: "f5a7752d-f199-43f8-9758-6ec74540e6cb", + tasks: [ + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + asyncComplete: false, + http_request: { + method: "GET", + uri: "http://ip-api.com/json/ 49.37.209.163?fields=status,message,country,countryCode,region,regionName,city,zip,lat,lon,timezone,offset,isp,org,as,query", + }, + }, + referenceTaskName: "get_IP_ref", + retryCount: 0, + seq: 1, + correlationId: "", + pollCount: 1, + taskDefName: "Get_IP", + scheduledTime: 1648555003459, + startTime: 1648555003591, + endTime: 1648555003633, + updateTime: 1648555003633, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "f5a7752d-f199-43f8-9758-6ec74540e6cb", + workflowType: "Stack_overflow_sequential_http", + taskId: "1ae10dc7-f391-48a7-8ab1-726a5c6b3752", + callbackAfterSeconds: 0, + workerId: "orkes-conductor-deployment-8d6c9d4bf-dfhfx", + outputData: { + response: { + headers: { + Date: ["Tue, 29 Mar 2022 11:56:43 GMT"], + "Content-Type": ["application/json; charset=utf-8"], + "Content-Length": ["343"], + "Access-Control-Allow-Origin": ["*"], + "X-Ttl": ["60"], + "X-Rl": ["44"], + }, + reasonPhrase: "OK", + body: { + status: "success", + country: "India", + countryCode: "IN", + region: "TN", + regionName: "Tamil Nadu", + city: "Chennai", + zip: "600001", + lat: 12.8996, + lon: 80.2209, + timezone: "Asia/Kolkata", + offset: 19800, + isp: "Reliance Jio Infocomm Limited", + org: "Reliance Jio Infocomm Limited", + as: "AS55836 Reliance Jio Infocomm Limited", + query: "49.37.209.163", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "Get_IP", + taskReferenceName: "get_IP_ref", + inputParameters: { + http_request: { + uri: "http://ip-api.com/json/${workflow.input.ipaddress}?fields=status,message,country,countryCode,region,regionName,city,zip,lat,lon,timezone,offset,isp,org,as,query", + method: "GET", + }, + asyncComplete: false, + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + taskDefinition: { + createTime: 1646667646103, + createdBy: "", + name: "Get_IP", + description: + "Edit or extend this sample task. Set the task name to get started", + retryCount: 3, + timeoutSeconds: 3600, + inputKeys: [], + outputKeys: [], + timeoutPolicy: "TIME_OUT_WF", + retryLogic: "FIXED", + retryDelaySeconds: 5, + responseTimeoutSeconds: 5, + inputTemplate: {}, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + backoffScaleFactor: 1, + }, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + retryCount: 3, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + taskDefinition: { + createTime: 1646667646103, + createdBy: "", + name: "Get_IP", + description: + "Edit or extend this sample task. Set the task name to get started", + retryCount: 3, + timeoutSeconds: 3600, + inputKeys: [], + outputKeys: [], + timeoutPolicy: "TIME_OUT_WF", + retryLogic: "FIXED", + retryDelaySeconds: 5, + responseTimeoutSeconds: 5, + inputTemplate: {}, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + backoffScaleFactor: 1, + }, + loopOverTask: false, + queueWaitTime: 132, + }, + { + taskType: "HTTP", + status: "FAILED", + inputData: { + asyncComplete: false, + http_request: { + method: "GET", + readTimeOut: 2000, + uri: "https://weatherdbi.herokuapp.com/data/weather/600001", + connectionTimeOut: 2000, + }, + zip_code: "600001", + }, + referenceTaskName: "get_weather_ref", + retryCount: 0, + seq: 2, + correlationId: "", + pollCount: 1, + taskDefName: "Get_weather", + scheduledTime: 1648555003635, + startTime: 1648555003876, + endTime: 1648555004023, + updateTime: 1648555004025, + startDelayInSeconds: 0, + retried: true, + executed: false, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "f5a7752d-f199-43f8-9758-6ec74540e6cb", + workflowType: "Stack_overflow_sequential_http", + taskId: "33c64585-455f-49e9-81b7-d2996158e58c", + reasonForIncompletion: + 'Failed to invoke HTTP task due to: java.lang.Exception: 503 Service Unavailable: "\t\t \t\t\t\t\t\tApplication Error\t\t\t \t \t\t\t \t"', + }, + workflowTask: { + name: "Get_weather", + taskReferenceName: "get_weather_ref", + inputParameters: { + zip_code: "${get_IP_ref.output.response.body.zip}", + http_request: { + uri: "https://weatherdbi.herokuapp.com/data/weather/${get_IP_ref.output.response.body.zip}", + method: "GET", + connectionTimeOut: 2000, + readTimeOut: 2000, + }, + asyncComplete: false, + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + taskDefinition: { + createTime: 1646667682201, + updateTime: 1646676973253, + createdBy: "", + updatedBy: "", + name: "Get_weather", + description: + "Edit or extend this sample task. Set the task name to get started", + retryCount: 3, + timeoutSeconds: 5, + inputKeys: [], + outputKeys: [], + timeoutPolicy: "TIME_OUT_WF", + retryLogic: "FIXED", + retryDelaySeconds: 5, + responseTimeoutSeconds: 5, + inputTemplate: {}, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + backoffScaleFactor: 1, + }, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + taskDefinition: { + createTime: 1646667682201, + updateTime: 1646676973253, + createdBy: "", + updatedBy: "", + name: "Get_weather", + description: + "Edit or extend this sample task. Set the task name to get started", + retryCount: 3, + timeoutSeconds: 5, + inputKeys: [], + outputKeys: [], + timeoutPolicy: "TIME_OUT_WF", + retryLogic: "FIXED", + retryDelaySeconds: 5, + responseTimeoutSeconds: 5, + inputTemplate: {}, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + backoffScaleFactor: 1, + }, + loopOverTask: false, + queueWaitTime: 241, + }, + { + taskType: "HTTP", + status: "FAILED", + inputData: { + http_request: { + method: "GET", + readTimeOut: 2000, + uri: "https://weatherdbi.herokuapp.com/data/weather/600001", + connectionTimeOut: 2000, + }, + asyncComplete: false, + zip_code: "600001", + }, + referenceTaskName: "get_weather_ref", + retryCount: 1, + seq: 3, + correlationId: "", + pollCount: 1, + taskDefName: "Get_weather", + scheduledTime: 1648555004046, + startTime: 1648555009163, + endTime: 1648555009232, + updateTime: 1648555004025, + startDelayInSeconds: 5, + retriedTaskId: "33c64585-455f-49e9-81b7-d2996158e58c", + retried: true, + executed: false, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "f5a7752d-f199-43f8-9758-6ec74540e6cb", + workflowType: "Stack_overflow_sequential_http", + taskId: "8c3b1231-a208-41dd-8828-83b24bf2806a", + reasonForIncompletion: + 'Failed to invoke HTTP task due to: java.lang.Exception: 503 Service Unavailable: "\t\t \t\t\t\t\t\tApplication Error\t\t\t \t \t\t\t \t"', + }, + workflowTask: { + name: "Get_weather", + taskReferenceName: "get_weather_ref", + inputParameters: { + zip_code: "${get_IP_ref.output.response.body.zip}", + http_request: { + uri: "https://weatherdbi.herokuapp.com/data/weather/${get_IP_ref.output.response.body.zip}", + method: "GET", + connectionTimeOut: 2000, + readTimeOut: 2000, + }, + asyncComplete: false, + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + taskDefinition: { + createTime: 1646667682201, + updateTime: 1646676973253, + createdBy: "", + updatedBy: "", + name: "Get_weather", + description: + "Edit or extend this sample task. Set the task name to get started", + retryCount: 3, + timeoutSeconds: 5, + inputKeys: [], + outputKeys: [], + timeoutPolicy: "TIME_OUT_WF", + retryLogic: "FIXED", + retryDelaySeconds: 5, + responseTimeoutSeconds: 5, + inputTemplate: {}, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + backoffScaleFactor: 1, + }, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + taskDefinition: { + createTime: 1646667682201, + updateTime: 1646676973253, + createdBy: "", + updatedBy: "", + name: "Get_weather", + description: + "Edit or extend this sample task. Set the task name to get started", + retryCount: 3, + timeoutSeconds: 5, + inputKeys: [], + outputKeys: [], + timeoutPolicy: "TIME_OUT_WF", + retryLogic: "FIXED", + retryDelaySeconds: 5, + responseTimeoutSeconds: 5, + inputTemplate: {}, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + backoffScaleFactor: 1, + }, + loopOverTask: false, + queueWaitTime: 191671401, + }, + { + taskType: "HTTP", + status: "FAILED", + inputData: { + http_request: { + method: "GET", + readTimeOut: 2000, + uri: "https://weatherdbi.herokuapp.com/data/weather/600001", + connectionTimeOut: 2000, + }, + asyncComplete: false, + zip_code: "600001", + }, + referenceTaskName: "get_weather_ref", + retryCount: 2, + seq: 4, + correlationId: "", + pollCount: 1, + taskDefName: "Get_weather", + scheduledTime: 1648555009247, + startTime: 1648555014285, + endTime: 1648555014407, + updateTime: 1648555004025, + startDelayInSeconds: 5, + retriedTaskId: "8c3b1231-a208-41dd-8828-83b24bf2806a", + retried: true, + executed: false, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "f5a7752d-f199-43f8-9758-6ec74540e6cb", + workflowType: "Stack_overflow_sequential_http", + taskId: "3886076f-6b38-4ce5-a099-c648e07546f8", + reasonForIncompletion: + 'Failed to invoke HTTP task due to: java.lang.Exception: 503 Service Unavailable: "\t\t \t\t\t\t\t\tApplication Error\t\t\t \t \t\t\t \t"', + }, + workflowTask: { + name: "Get_weather", + taskReferenceName: "get_weather_ref", + inputParameters: { + zip_code: "${get_IP_ref.output.response.body.zip}", + http_request: { + uri: "https://weatherdbi.herokuapp.com/data/weather/${get_IP_ref.output.response.body.zip}", + method: "GET", + connectionTimeOut: 2000, + readTimeOut: 2000, + }, + asyncComplete: false, + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + taskDefinition: { + createTime: 1646667682201, + updateTime: 1646676973253, + createdBy: "", + updatedBy: "", + name: "Get_weather", + description: + "Edit or extend this sample task. Set the task name to get started", + retryCount: 3, + timeoutSeconds: 5, + inputKeys: [], + outputKeys: [], + timeoutPolicy: "TIME_OUT_WF", + retryLogic: "FIXED", + retryDelaySeconds: 5, + responseTimeoutSeconds: 5, + inputTemplate: {}, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + backoffScaleFactor: 1, + }, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + taskDefinition: { + createTime: 1646667682201, + updateTime: 1646676973253, + createdBy: "", + updatedBy: "", + name: "Get_weather", + description: + "Edit or extend this sample task. Set the task name to get started", + retryCount: 3, + timeoutSeconds: 5, + inputKeys: [], + outputKeys: [], + timeoutPolicy: "TIME_OUT_WF", + retryLogic: "FIXED", + retryDelaySeconds: 5, + responseTimeoutSeconds: 5, + inputTemplate: {}, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + backoffScaleFactor: 1, + }, + loopOverTask: false, + queueWaitTime: 191671402, + }, + { + taskType: "HTTP", + status: "FAILED", + inputData: { + http_request: { + method: "GET", + readTimeOut: 2000, + uri: "https://weatherdbi.herokuapp.com/data/weather/600001", + connectionTimeOut: 2000, + }, + asyncComplete: false, + zip_code: "600001", + }, + referenceTaskName: "get_weather_ref", + retryCount: 3, + seq: 5, + correlationId: "", + pollCount: 1, + taskDefName: "Get_weather", + scheduledTime: 1648555014423, + startTime: 1648555019511, + endTime: 1648555019608, + updateTime: 1648555004025, + startDelayInSeconds: 5, + retriedTaskId: "3886076f-6b38-4ce5-a099-c648e07546f8", + retried: true, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "f5a7752d-f199-43f8-9758-6ec74540e6cb", + workflowType: "Stack_overflow_sequential_http", + taskId: "e7fb9cff-2a8d-4cc8-9cfe-764138980f21", + reasonForIncompletion: + 'Failed to invoke HTTP task due to: java.lang.Exception: 503 Service Unavailable: "\t\t \t\t\t\t\t\tApplication Error\t\t\t \t \t\t\t \t"', + }, + workflowTask: { + name: "Get_weather", + taskReferenceName: "get_weather_ref", + inputParameters: { + zip_code: "${get_IP_ref.output.response.body.zip}", + http_request: { + uri: "https://weatherdbi.herokuapp.com/data/weather/${get_IP_ref.output.response.body.zip}", + method: "GET", + connectionTimeOut: 2000, + readTimeOut: 2000, + }, + asyncComplete: false, + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + taskDefinition: { + createTime: 1646667682201, + updateTime: 1646676973253, + createdBy: "", + updatedBy: "", + name: "Get_weather", + description: + "Edit or extend this sample task. Set the task name to get started", + retryCount: 3, + timeoutSeconds: 5, + inputKeys: [], + outputKeys: [], + timeoutPolicy: "TIME_OUT_WF", + retryLogic: "FIXED", + retryDelaySeconds: 5, + responseTimeoutSeconds: 5, + inputTemplate: {}, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + backoffScaleFactor: 1, + }, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + taskDefinition: { + createTime: 1646667682201, + updateTime: 1646676973253, + createdBy: "", + updatedBy: "", + name: "Get_weather", + description: + "Edit or extend this sample task. Set the task name to get started", + retryCount: 3, + timeoutSeconds: 5, + inputKeys: [], + outputKeys: [], + timeoutPolicy: "TIME_OUT_WF", + retryLogic: "FIXED", + retryDelaySeconds: 5, + responseTimeoutSeconds: 5, + inputTemplate: {}, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + backoffScaleFactor: 1, + }, + loopOverTask: false, + queueWaitTime: 191671402, + }, + { + taskType: "HTTP", + status: "FAILED", + inputData: { + http_request: { + method: "GET", + readTimeOut: 2000, + uri: "https://weatherdbi.herokuapp.com/data/weather/600001", + connectionTimeOut: 2000, + }, + asyncComplete: false, + zip_code: "600001", + }, + referenceTaskName: "get_weather_ref", + retryCount: 4, + seq: 6, + correlationId: "", + pollCount: 1, + taskDefName: "Get_weather", + scheduledTime: 1648576362044, + startTime: 1648576362081, + endTime: 1648576362213, + updateTime: 1648576362039, + startDelayInSeconds: 5, + retriedTaskId: "e7fb9cff-2a8d-4cc8-9cfe-764138980f21", + retried: true, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "f5a7752d-f199-43f8-9758-6ec74540e6cb", + workflowType: "Stack_overflow_sequential_http", + taskId: "95c468e5-259d-4f4a-a9da-d1f6d48153ef", + reasonForIncompletion: + 'Failed to invoke HTTP task due to: java.lang.Exception: 503 Service Unavailable: "\t\t \t\t\t\t\t\tApplication Error\t\t\t \t \t\t\t \t"', + }, + workflowTask: { + name: "Get_weather", + taskReferenceName: "get_weather_ref", + inputParameters: { + zip_code: "${get_IP_ref.output.response.body.zip}", + http_request: { + uri: "https://weatherdbi.herokuapp.com/data/weather/${get_IP_ref.output.response.body.zip}", + method: "GET", + connectionTimeOut: 2000, + readTimeOut: 2000, + }, + asyncComplete: false, + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + taskDefinition: { + createTime: 1646667682201, + updateTime: 1646676973253, + createdBy: "", + updatedBy: "", + name: "Get_weather", + description: + "Edit or extend this sample task. Set the task name to get started", + retryCount: 3, + timeoutSeconds: 5, + inputKeys: [], + outputKeys: [], + timeoutPolicy: "TIME_OUT_WF", + retryLogic: "FIXED", + retryDelaySeconds: 5, + responseTimeoutSeconds: 5, + inputTemplate: {}, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + backoffScaleFactor: 1, + }, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + taskDefinition: { + createTime: 1646667682201, + updateTime: 1646676973253, + createdBy: "", + updatedBy: "", + name: "Get_weather", + description: + "Edit or extend this sample task. Set the task name to get started", + retryCount: 3, + timeoutSeconds: 5, + inputKeys: [], + outputKeys: [], + timeoutPolicy: "TIME_OUT_WF", + retryLogic: "FIXED", + retryDelaySeconds: 5, + responseTimeoutSeconds: 5, + inputTemplate: {}, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + backoffScaleFactor: 1, + }, + loopOverTask: false, + queueWaitTime: 37, + }, + { + taskType: "HTTP", + status: "FAILED", + inputData: { + http_request: { + method: "GET", + readTimeOut: 2000, + uri: "https://weatherdbi.herokuapp.com/data/weather/600001", + connectionTimeOut: 2000, + }, + asyncComplete: false, + zip_code: "600001", + }, + referenceTaskName: "get_weather_ref", + retryCount: 5, + seq: 7, + correlationId: "", + pollCount: 1, + taskDefName: "Get_weather", + scheduledTime: 1648585762469, + startTime: 1648585762599, + endTime: 1648585762712, + updateTime: 1648585762466, + startDelayInSeconds: 5, + retriedTaskId: "95c468e5-259d-4f4a-a9da-d1f6d48153ef", + retried: false, + executed: false, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "f5a7752d-f199-43f8-9758-6ec74540e6cb", + workflowType: "Stack_overflow_sequential_http", + taskId: "5c61b913-5883-4725-8d39-0edd3029cbaf", + reasonForIncompletion: + 'Failed to invoke HTTP task due to: java.lang.Exception: 503 Service Unavailable: "\t\t \t\t\t\t\t\tApplication Error\t\t\t \t \t\t\t \t"', + }, + workflowTask: { + name: "Get_weather", + taskReferenceName: "get_weather_ref", + inputParameters: { + zip_code: "${get_IP_ref.output.response.body.zip}", + http_request: { + uri: "https://weatherdbi.herokuapp.com/data/weather/${get_IP_ref.output.response.body.zip}", + method: "GET", + connectionTimeOut: 2000, + readTimeOut: 2000, + }, + asyncComplete: false, + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + taskDefinition: { + createTime: 1646667682201, + updateTime: 1646676973253, + createdBy: "", + updatedBy: "", + name: "Get_weather", + description: + "Edit or extend this sample task. Set the task name to get started", + retryCount: 3, + timeoutSeconds: 5, + inputKeys: [], + outputKeys: [], + timeoutPolicy: "TIME_OUT_WF", + retryLogic: "FIXED", + retryDelaySeconds: 5, + responseTimeoutSeconds: 5, + inputTemplate: {}, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + backoffScaleFactor: 1, + }, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + taskDefinition: { + createTime: 1646667682201, + updateTime: 1646676973253, + createdBy: "", + updatedBy: "", + name: "Get_weather", + description: + "Edit or extend this sample task. Set the task name to get started", + retryCount: 3, + timeoutSeconds: 5, + inputKeys: [], + outputKeys: [], + timeoutPolicy: "TIME_OUT_WF", + retryLogic: "FIXED", + retryDelaySeconds: 5, + responseTimeoutSeconds: 5, + inputTemplate: {}, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + backoffScaleFactor: 1, + }, + loopOverTask: false, + queueWaitTime: 130, + }, + ], + input: { + ipaddress: " 49.37.209.163", + }, + output: { + zipcode: "600001", + forecast: null, + }, + correlationId: "", + reasonForIncompletion: + 'Failed to invoke HTTP task due to: java.lang.Exception: 503 Service Unavailable: "\t\t \t\t\t\t\t\tApplication Error\t\t\t \t \t\t\t \t"', + taskToDomain: {}, + failedReferenceTaskNames: ["get_weather_ref"], + workflowDefinition: { + updateTime: 1647385959054, + name: "Stack_overflow_sequential_http", + description: + "Answering https://stackoverflow.com/questions/71370237/java-design-pattern-orchestration-workflow", + version: 1, + tasks: [ + { + name: "Get_IP", + taskReferenceName: "get_IP_ref", + inputParameters: { + http_request: { + uri: "http://ip-api.com/json/${workflow.input.ipaddress}?fields=status,message,country,countryCode,region,regionName,city,zip,lat,lon,timezone,offset,isp,org,as,query", + method: "GET", + }, + asyncComplete: false, + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + taskDefinition: { + createTime: 1646667646103, + createdBy: "", + name: "Get_IP", + description: + "Edit or extend this sample task. Set the task name to get started", + retryCount: 3, + timeoutSeconds: 3600, + inputKeys: [], + outputKeys: [], + timeoutPolicy: "TIME_OUT_WF", + retryLogic: "FIXED", + retryDelaySeconds: 5, + responseTimeoutSeconds: 5, + inputTemplate: {}, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + backoffScaleFactor: 1, + }, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + retryCount: 3, + }, + { + name: "Get_weather", + taskReferenceName: "get_weather_ref", + inputParameters: { + zip_code: "${get_IP_ref.output.response.body.zip}", + http_request: { + uri: "https://weatherdbi.herokuapp.com/data/weather/${get_IP_ref.output.response.body.zip}", + method: "GET", + connectionTimeOut: 2000, + readTimeOut: 2000, + }, + asyncComplete: false, + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + taskDefinition: { + createTime: 1646667682201, + updateTime: 1646676973253, + createdBy: "", + updatedBy: "", + name: "Get_weather", + description: + "Edit or extend this sample task. Set the task name to get started", + retryCount: 3, + timeoutSeconds: 5, + inputKeys: [], + outputKeys: [], + timeoutPolicy: "TIME_OUT_WF", + retryLogic: "FIXED", + retryDelaySeconds: 5, + responseTimeoutSeconds: 5, + inputTemplate: {}, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + backoffScaleFactor: 1, + }, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + inputParameters: [], + outputParameters: { + zipcode: "${get_IP_ref.output.response.body.zip}", + forecast: + "${get_weather_ref.output.response.body.currentConditions.comment}", + }, + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: false, + ownerEmail: "example@email.com", + timeoutPolicy: "ALERT_ONLY", + timeoutSeconds: 0, + failureWorkflow: "", + variables: {}, + inputTemplate: {}, + }, + priority: 0, + variables: {}, + lastRetriedTime: 1648585762439, + startTime: 1648555003451, + workflowName: "Stack_overflow_sequential_http", + workflowVersion: 1, +}; + +export const sampleExecutionWithForkJoin = { + ownerApp: "", + createTime: 1654516799948, + createdBy: "doug.sillars@orkes.io", + status: "COMPLETED", + endTime: 1654516826244, + workflowId: "30dcf124-e590-11ec-99fe-ea3af9c7af2a", + tasks: [ + { + taskType: "JSON_JQ_TRANSFORM", + status: "COMPLETED", + inputData: { + input: [ + { + numberOfWidgets: "12", + name: "Bob McBobFace", + street: "21 Bob Lane", + city: "Bobville", + state: "OR", + zip: "53111", + }, + { + numberOfWidgets: "1", + name: "BobBobBob BobraAnn", + street: "1 Surf Street", + city: "Kokomo", + state: "FL", + zip: "53111", + }, + ], + queryExpression: ".[] |length", + }, + referenceTaskName: "jq_address_count_ref", + retryCount: 0, + seq: 1, + pollCount: 0, + taskDefName: "jq_address_count", + scheduledTime: 1654516799963, + startTime: 1654516799963, + endTime: 1654516799970, + updateTime: 1654516799970, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "30dcf124-e590-11ec-99fe-ea3af9c7af2a", + workflowType: "Bobs_widget_fulfillment", + taskId: "30df1405-e590-11ec-99fe-ea3af9c7af2a", + callbackAfterSeconds: 0, + outputData: { + result: 2, + resultList: [2, 11], + }, + workflowTask: { + name: "jq_address_count", + taskReferenceName: "jq_address_count_ref", + inputParameters: { + input: "${workflow.input.addressList}", + queryExpression: ".[] |length", + }, + type: "JSON_JQ_TRANSFORM", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + taskDefinition: null, + loopOverTask: false, + queueWaitTime: 0, + }, + { + taskType: "JSON_JQ_TRANSFORM", + status: "COMPLETED", + inputData: { + input: "{}", + queryExpression: + 'reduce range(0,2) as $f (.; .dynamicTasks[$f].subWorkflowParam.name = "Shipping_loop_workflow" | .dynamicTasks[$f].taskReferenceName = "shipping_loop_subworkflow_ref_\\($f)" | .dynamicTasks[$f].type = "SUB_WORKFLOW")', + }, + referenceTaskName: "jq_create_dynamictasks_ref", + retryCount: 0, + seq: 2, + pollCount: 0, + taskDefName: "jq_create_dynamictasks", + scheduledTime: 1654516799990, + startTime: 1654516799990, + endTime: 1654516799998, + updateTime: 1654516799998, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "30dcf124-e590-11ec-99fe-ea3af9c7af2a", + workflowType: "Bobs_widget_fulfillment", + taskId: "30e332b6-e590-11ec-99fe-ea3af9c7af2a", + callbackAfterSeconds: 0, + outputData: { + result: { + input: "{}", + queryExpression: + 'reduce range(0,2) as $f (.; .dynamicTasks[$f].subWorkflowParam.name = "Shipping_loop_workflow" | .dynamicTasks[$f].taskReferenceName = "shipping_loop_subworkflow_ref_\\($f)" | .dynamicTasks[$f].type = "SUB_WORKFLOW")', + dynamicTasks: [ + { + subWorkflowParam: { + name: "Shipping_loop_workflow", + }, + taskReferenceName: "shipping_loop_subworkflow_ref_0", + type: "SUB_WORKFLOW", + }, + { + subWorkflowParam: { + name: "Shipping_loop_workflow", + }, + taskReferenceName: "shipping_loop_subworkflow_ref_1", + type: "SUB_WORKFLOW", + }, + ], + }, + resultList: [ + { + input: "{}", + queryExpression: + 'reduce range(0,2) as $f (.; .dynamicTasks[$f].subWorkflowParam.name = "Shipping_loop_workflow" | .dynamicTasks[$f].taskReferenceName = "shipping_loop_subworkflow_ref_\\($f)" | .dynamicTasks[$f].type = "SUB_WORKFLOW")', + dynamicTasks: [ + { + subWorkflowParam: { + name: "Shipping_loop_workflow", + }, + taskReferenceName: "shipping_loop_subworkflow_ref_0", + type: "SUB_WORKFLOW", + }, + { + subWorkflowParam: { + name: "Shipping_loop_workflow", + }, + taskReferenceName: "shipping_loop_subworkflow_ref_1", + type: "SUB_WORKFLOW", + }, + ], + }, + ], + }, + workflowTask: { + name: "jq_create_dynamictasks", + taskReferenceName: "jq_create_dynamictasks_ref", + inputParameters: { + input: "{}", + queryExpression: + 'reduce range(0,${jq_address_count_ref.output.result}) as $f (.; .dynamicTasks[$f].subWorkflowParam.name = "Shipping_loop_workflow" | .dynamicTasks[$f].taskReferenceName = "shipping_loop_subworkflow_ref_\\($f)" | .dynamicTasks[$f].type = "SUB_WORKFLOW")', + }, + type: "JSON_JQ_TRANSFORM", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + taskDefinition: null, + loopOverTask: false, + queueWaitTime: 0, + }, + { + taskType: "JSON_JQ_TRANSFORM", + status: "COMPLETED", + inputData: { + input: "{}", + addresses: [ + { + numberOfWidgets: "12", + name: "Bob McBobFace", + street: "21 Bob Lane", + city: "Bobville", + state: "OR", + zip: "53111", + }, + { + numberOfWidgets: "1", + name: "BobBobBob BobraAnn", + street: "1 Surf Street", + city: "Kokomo", + state: "FL", + zip: "53111", + }, + ], + taskList: { + input: "{}", + queryExpression: + 'reduce range(0,2) as $f (.; .dynamicTasks[$f].subWorkflowParam.name = "Shipping_loop_workflow" | .dynamicTasks[$f].taskReferenceName = "shipping_loop_subworkflow_ref_\\($f)" | .dynamicTasks[$f].type = "SUB_WORKFLOW")', + dynamicTasks: [ + { + subWorkflowParam: { + name: "Shipping_loop_workflow", + }, + taskReferenceName: "shipping_loop_subworkflow_ref_0", + type: "SUB_WORKFLOW", + }, + { + subWorkflowParam: { + name: "Shipping_loop_workflow", + }, + taskReferenceName: "shipping_loop_subworkflow_ref_1", + type: "SUB_WORKFLOW", + }, + ], + }, + queryExpression: + 'reduce range(0,2) as $f (.; .dynamicTasksInput."shipping_loop_subworkflow_ref_\\($f)" = .addresses[$f])', + }, + referenceTaskName: "jq_create_dynamictasksParams_ref", + retryCount: 0, + seq: 3, + pollCount: 0, + taskDefName: "jq_create_dynamictaskParams", + scheduledTime: 1654516800029, + startTime: 1654516800029, + endTime: 1654516800037, + updateTime: 1654516800037, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "30dcf124-e590-11ec-99fe-ea3af9c7af2a", + workflowType: "Bobs_widget_fulfillment", + taskId: "30e92627-e590-11ec-99fe-ea3af9c7af2a", + callbackAfterSeconds: 0, + outputData: { + result: { + input: "{}", + addresses: [ + { + numberOfWidgets: "12", + name: "Bob McBobFace", + street: "21 Bob Lane", + city: "Bobville", + state: "OR", + zip: "53111", + }, + { + numberOfWidgets: "1", + name: "BobBobBob BobraAnn", + street: "1 Surf Street", + city: "Kokomo", + state: "FL", + zip: "53111", + }, + ], + taskList: { + input: "{}", + queryExpression: + 'reduce range(0,2) as $f (.; .dynamicTasks[$f].subWorkflowParam.name = "Shipping_loop_workflow" | .dynamicTasks[$f].taskReferenceName = "shipping_loop_subworkflow_ref_\\($f)" | .dynamicTasks[$f].type = "SUB_WORKFLOW")', + dynamicTasks: [ + { + subWorkflowParam: { + name: "Shipping_loop_workflow", + }, + taskReferenceName: "shipping_loop_subworkflow_ref_0", + type: "SUB_WORKFLOW", + }, + { + subWorkflowParam: { + name: "Shipping_loop_workflow", + }, + taskReferenceName: "shipping_loop_subworkflow_ref_1", + type: "SUB_WORKFLOW", + }, + ], + }, + queryExpression: + 'reduce range(0,2) as $f (.; .dynamicTasksInput."shipping_loop_subworkflow_ref_\\($f)" = .addresses[$f])', + dynamicTasksInput: { + shipping_loop_subworkflow_ref_0: { + numberOfWidgets: "12", + name: "Bob McBobFace", + street: "21 Bob Lane", + city: "Bobville", + state: "OR", + zip: "53111", + }, + shipping_loop_subworkflow_ref_1: { + numberOfWidgets: "1", + name: "BobBobBob BobraAnn", + street: "1 Surf Street", + city: "Kokomo", + state: "FL", + zip: "53111", + }, + }, + }, + resultList: [ + { + input: "{}", + addresses: [ + { + numberOfWidgets: "12", + name: "Bob McBobFace", + street: "21 Bob Lane", + city: "Bobville", + state: "OR", + zip: "53111", + }, + { + numberOfWidgets: "1", + name: "BobBobBob BobraAnn", + street: "1 Surf Street", + city: "Kokomo", + state: "FL", + zip: "53111", + }, + ], + taskList: { + input: "{}", + queryExpression: + 'reduce range(0,2) as $f (.; .dynamicTasks[$f].subWorkflowParam.name = "Shipping_loop_workflow" | .dynamicTasks[$f].taskReferenceName = "shipping_loop_subworkflow_ref_\\($f)" | .dynamicTasks[$f].type = "SUB_WORKFLOW")', + dynamicTasks: [ + { + subWorkflowParam: { + name: "Shipping_loop_workflow", + }, + taskReferenceName: "shipping_loop_subworkflow_ref_0", + type: "SUB_WORKFLOW", + }, + { + subWorkflowParam: { + name: "Shipping_loop_workflow", + }, + taskReferenceName: "shipping_loop_subworkflow_ref_1", + type: "SUB_WORKFLOW", + }, + ], + }, + queryExpression: + 'reduce range(0,2) as $f (.; .dynamicTasksInput."shipping_loop_subworkflow_ref_\\($f)" = .addresses[$f])', + dynamicTasksInput: { + shipping_loop_subworkflow_ref_0: { + numberOfWidgets: "12", + name: "Bob McBobFace", + street: "21 Bob Lane", + city: "Bobville", + state: "OR", + zip: "53111", + }, + shipping_loop_subworkflow_ref_1: { + numberOfWidgets: "1", + name: "BobBobBob BobraAnn", + street: "1 Surf Street", + city: "Kokomo", + state: "FL", + zip: "53111", + }, + }, + }, + ], + }, + workflowTask: { + name: "jq_create_dynamictaskParams", + taskReferenceName: "jq_create_dynamictasksParams_ref", + inputParameters: { + input: "{}", + addresses: "${workflow.input.addressList}", + taskList: "${jq_create_dynamictasks_ref.output.result}", + queryExpression: + 'reduce range(0,${jq_address_count_ref.output.result}) as $f (.; .dynamicTasksInput."shipping_loop_subworkflow_ref_\\($f)" = .addresses[$f])', + }, + type: "JSON_JQ_TRANSFORM", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + taskDefinition: null, + loopOverTask: false, + queueWaitTime: 0, + }, + { + taskType: "FORK", + status: "COMPLETED", + inputData: { + forkedTaskDefs: [ + { + taskReferenceName: "shipping_loop_subworkflow_ref_0", + inputParameters: {}, + type: "SUB_WORKFLOW", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + subWorkflowParam: { + name: "Shipping_loop_workflow", + }, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + taskReferenceName: "shipping_loop_subworkflow_ref_1", + inputParameters: {}, + type: "SUB_WORKFLOW", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + subWorkflowParam: { + name: "Shipping_loop_workflow", + }, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + forkedTasks: [ + "shipping_loop_subworkflow_ref_0", + "shipping_loop_subworkflow_ref_1", + ], + }, + referenceTaskName: "shipping_dynamic_fork_ref", + retryCount: 0, + seq: 4, + pollCount: 0, + taskDefName: "FORK", + scheduledTime: 1654516800114, + startTime: 1654516800114, + endTime: 1654516800114, + updateTime: 1654516800175, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "30dcf124-e590-11ec-99fe-ea3af9c7af2a", + workflowType: "Bobs_widget_fulfillment", + taskId: "30f5f768-e590-11ec-99fe-ea3af9c7af2a", + callbackAfterSeconds: 0, + outputData: {}, + workflowTask: { + name: "shipping_dynamic_fork", + taskReferenceName: "shipping_dynamic_fork_ref", + inputParameters: { + dynamicTasks: + "${jq_create_dynamictasks_ref.output.result.dynamicTasks}", + dynamicTasksInput: + "${jq_create_dynamictasksParams_ref.output.result.dynamicTasksInput}", + }, + type: "FORK_JOIN_DYNAMIC", + decisionCases: {}, + dynamicForkTasksParam: "dynamicTasks", + dynamicForkTasksInputParamName: "dynamicTasksInput", + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + taskDefinition: null, + loopOverTask: false, + queueWaitTime: 0, + }, + { + taskType: "SUB_WORKFLOW", + status: "COMPLETED", + inputData: { + zip: "53111", + subWorkflowDefinition: null, + workflowInput: {}, + city: "Bobville", + subWorkflowTaskToDomain: null, + street: "21 Bob Lane", + subWorkflowName: "Shipping_loop_workflow", + name: "Bob McBobFace", + state: "OR", + subWorkflowVersion: 1, + numberOfWidgets: "12", + }, + referenceTaskName: "shipping_loop_subworkflow_ref_0", + retryCount: 0, + seq: 5, + pollCount: 1, + taskDefName: "SUB_WORKFLOW", + scheduledTime: 1654516800119, + startTime: 1654516800623, + endTime: 1654516813323, + updateTime: 1654516800671, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "30dcf124-e590-11ec-99fe-ea3af9c7af2a", + workflowType: "Bobs_widget_fulfillment", + taskId: "30f64589-e590-11ec-99fe-ea3af9c7af2a", + callbackAfterSeconds: 0, + outputData: { + subWorkflowId: "31443e81-e590-11ec-99fe-ea3af9c7af2a", + orderDetails: { + 1: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "81900100 68490647", + }, + }, + 2: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "75954032 76216878", + }, + }, + 3: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "52750497 19652062", + }, + }, + 4: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "72896512 69020693", + }, + }, + 5: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "61473582 94684741", + }, + }, + 6: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "49293897 64849148", + }, + }, + 7: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "72669959 93938409", + }, + }, + 8: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "39018420 85980274", + }, + }, + 9: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "8274183 9267380", + }, + }, + 10: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "1884788 19309914", + }, + }, + 11: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "29288059 71566372", + }, + }, + 12: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "44861411 88039618", + }, + }, + iteration: 12, + }, + }, + workflowTask: { + taskReferenceName: "shipping_loop_subworkflow_ref_0", + inputParameters: {}, + type: "SUB_WORKFLOW", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + subWorkflowParam: { + name: "Shipping_loop_workflow", + }, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subWorkflowId: "31443e81-e590-11ec-99fe-ea3af9c7af2a", + subworkflowChanged: false, + taskDefinition: null, + loopOverTask: false, + queueWaitTime: 504, + }, + { + taskType: "SUB_WORKFLOW", + status: "COMPLETED", + inputData: { + zip: "53111", + subWorkflowDefinition: null, + workflowInput: {}, + city: "Kokomo", + subWorkflowTaskToDomain: null, + street: "1 Surf Street", + subWorkflowName: "Shipping_loop_workflow", + name: "BobBobBob BobraAnn", + state: "FL", + subWorkflowVersion: 1, + numberOfWidgets: "1", + }, + referenceTaskName: "shipping_loop_subworkflow_ref_1", + retryCount: 0, + seq: 6, + pollCount: 1, + taskDefName: "SUB_WORKFLOW", + scheduledTime: 1654516800133, + startTime: 1654516800673, + endTime: 1654516802428, + updateTime: 1654516800720, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "30dcf124-e590-11ec-99fe-ea3af9c7af2a", + workflowType: "Bobs_widget_fulfillment", + taskId: "30f708da-e590-11ec-99fe-ea3af9c7af2a", + callbackAfterSeconds: 0, + outputData: { + subWorkflowId: "314bdfa4-e590-11ec-99fe-ea3af9c7af2a", + orderDetails: { + 1: { + widget_shipping: { + fullAddress: + "BobBobBob BobraAnn\n1 Surf Street\nKokomo, FL 53111", + trackingNumber: "5500341 47648420", + }, + }, + iteration: 1, + }, + }, + workflowTask: { + taskReferenceName: "shipping_loop_subworkflow_ref_1", + inputParameters: {}, + type: "SUB_WORKFLOW", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + subWorkflowParam: { + name: "Shipping_loop_workflow", + }, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subWorkflowId: "314bdfa4-e590-11ec-99fe-ea3af9c7af2a", + subworkflowChanged: false, + taskDefinition: null, + loopOverTask: false, + queueWaitTime: 540, + }, + { + taskType: "JOIN", + status: "COMPLETED", + inputData: { + joinOn: [ + "shipping_loop_subworkflow_ref_0", + "shipping_loop_subworkflow_ref_1", + ], + }, + referenceTaskName: "image_multiple_convert_resize_join_ref", + retryCount: 0, + seq: 7, + pollCount: 0, + taskDefName: "JOIN", + scheduledTime: 1654516800133, + startTime: 1654516800133, + endTime: 1654516815698, + updateTime: 1654516800202, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "30dcf124-e590-11ec-99fe-ea3af9c7af2a", + workflowType: "Bobs_widget_fulfillment", + taskId: "30f92bbb-e590-11ec-99fe-ea3af9c7af2a", + callbackAfterSeconds: 0, + outputData: { + shipping_loop_subworkflow_ref_0: { + subWorkflowId: "31443e81-e590-11ec-99fe-ea3af9c7af2a", + orderDetails: { + 1: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "81900100 68490647", + }, + }, + 2: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "75954032 76216878", + }, + }, + 3: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "52750497 19652062", + }, + }, + 4: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "72896512 69020693", + }, + }, + 5: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "61473582 94684741", + }, + }, + 6: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "49293897 64849148", + }, + }, + 7: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "72669959 93938409", + }, + }, + 8: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "39018420 85980274", + }, + }, + 9: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "8274183 9267380", + }, + }, + 10: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "1884788 19309914", + }, + }, + 11: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "29288059 71566372", + }, + }, + 12: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "44861411 88039618", + }, + }, + iteration: 12, + }, + }, + shipping_loop_subworkflow_ref_1: { + subWorkflowId: "314bdfa4-e590-11ec-99fe-ea3af9c7af2a", + orderDetails: { + 1: { + widget_shipping: { + fullAddress: + "BobBobBob BobraAnn\n1 Surf Street\nKokomo, FL 53111", + trackingNumber: "5500341 47648420", + }, + }, + iteration: 1, + }, + }, + }, + workflowTask: { + name: "shipping_multiple_addresses_join", + taskReferenceName: "image_multiple_convert_resize_join_ref", + inputParameters: {}, + type: "JOIN", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + taskDefinition: null, + loopOverTask: false, + queueWaitTime: 0, + }, + { + taskType: "JSON_JQ_TRANSFORM", + status: "COMPLETED", + inputData: { + input: [ + { + numberOfWidgets: "12", + name: "Bob McBobFace", + street: "21 Bob Lane", + city: "Bobville", + state: "OR", + zip: "53111", + }, + { + numberOfWidgets: "1", + name: "BobBobBob BobraAnn", + street: "1 Surf Street", + city: "Kokomo", + state: "FL", + zip: "53111", + }, + ], + queryExpression: "[.input[].numberOfWidgets | tonumber ] | add", + }, + referenceTaskName: "jq_sum_widgets_ref", + retryCount: 0, + seq: 8, + pollCount: 0, + taskDefName: "jq_sum_widgets", + scheduledTime: 1654516815724, + startTime: 1654516815724, + endTime: 1654516815736, + updateTime: 1654516815736, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "30dcf124-e590-11ec-99fe-ea3af9c7af2a", + workflowType: "Bobs_widget_fulfillment", + taskId: "3a440421-e590-11ec-8ff3-3e1f34859ffe", + callbackAfterSeconds: 0, + outputData: { + result: 13, + resultList: [13], + }, + workflowTask: { + name: "jq_sum_widgets", + taskReferenceName: "jq_sum_widgets_ref", + inputParameters: { + input: "${workflow.input.addressList}", + queryExpression: "[.input[].numberOfWidgets | tonumber ] | add", + }, + type: "JSON_JQ_TRANSFORM", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + taskDefinition: null, + loopOverTask: false, + queueWaitTime: 0, + }, + { + taskType: "HTTP", + status: "FAILED", + inputData: { + asyncComplete: false, + http_request: { + method: "POST", + readTimeOut: 5000, + body: { + item: "widget", + count: 13, + }, + uri: "http://restfuldemo.herokuapp.com/appendorder", + connectionTimeOut: 5000, + }, + }, + referenceTaskName: "reorder_widgets_ref", + retryCount: 0, + seq: 9, + pollCount: 1, + taskDefName: "reorder_widgets", + scheduledTime: 1654516815761, + startTime: 1654516815873, + endTime: 1654516820969, + updateTime: 1654516820970, + startDelayInSeconds: 0, + retried: true, + executed: false, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "30dcf124-e590-11ec-99fe-ea3af9c7af2a", + workflowType: "Bobs_widget_fulfillment", + taskId: "3a49d082-e590-11ec-8ff3-3e1f34859ffe", + reasonForIncompletion: + 'Failed to invoke HTTP task due to: java.lang.Exception: I/O error on POST request for "http://restfuldemo.herokuapp.com/appendorder": Read timed out; nested exception is java.net.SocketTimeoutException: Read timed out', + callbackAfterSeconds: 0, + workerId: "orkes-conductor-deployment-6644848475-7rv6z", + outputData: { + response: + 'java.lang.Exception: I/O error on POST request for "http://restfuldemo.herokuapp.com/appendorder": Read timed out; nested exception is java.net.SocketTimeoutException: Read timed out', + }, + workflowTask: { + name: "reorder_widgets", + taskReferenceName: "reorder_widgets_ref", + inputParameters: { + http_request: { + uri: "http://restfuldemo.herokuapp.com/appendorder", + method: "POST", + body: { + item: "widget", + count: "${jq_sum_widgets_ref.output.result}", + }, + connectionTimeOut: 5000, + readTimeOut: 5000, + }, + asyncComplete: false, + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + taskDefinition: { + createTime: 1649342184551, + updateTime: 1649342255822, + createdBy: "", + updatedBy: "", + name: "reorder_widgets", + description: + "extending the reorder task to have 3 retries and fixed delay", + retryCount: 3, + timeoutSeconds: 10, + inputKeys: [], + outputKeys: [], + timeoutPolicy: "TIME_OUT_WF", + retryLogic: "FIXED", + retryDelaySeconds: 5, + responseTimeoutSeconds: 5, + inputTemplate: {}, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + ownerEmail: "doug.sillars@orkes.io", + backoffScaleFactor: 1, + }, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + retryCount: 3, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + taskDefinition: { + createTime: 1649342184551, + updateTime: 1649342255822, + createdBy: "", + updatedBy: "", + name: "reorder_widgets", + description: + "extending the reorder task to have 3 retries and fixed delay", + retryCount: 3, + timeoutSeconds: 10, + inputKeys: [], + outputKeys: [], + timeoutPolicy: "TIME_OUT_WF", + retryLogic: "FIXED", + retryDelaySeconds: 5, + responseTimeoutSeconds: 5, + inputTemplate: {}, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + ownerEmail: "doug.sillars@orkes.io", + backoffScaleFactor: 1, + }, + loopOverTask: false, + queueWaitTime: 112, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + asyncComplete: false, + http_request: { + method: "POST", + readTimeOut: 5000, + body: { + item: "widget", + count: 13, + }, + uri: "http://restfuldemo.herokuapp.com/appendorder", + connectionTimeOut: 5000, + }, + }, + referenceTaskName: "reorder_widgets_ref", + retryCount: 1, + seq: 10, + pollCount: 1, + taskDefName: "reorder_widgets", + scheduledTime: 1654516820980, + startTime: 1654516826123, + endTime: 1654516826164, + updateTime: 1654516820970, + startDelayInSeconds: 5, + retriedTaskId: "3a49d082-e590-11ec-8ff3-3e1f34859ffe", + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "30dcf124-e590-11ec-99fe-ea3af9c7af2a", + workflowType: "Bobs_widget_fulfillment", + taskId: "3d66047f-e590-11ec-8bc1-168cc4bae9cf", + callbackAfterSeconds: 5, + workerId: "orkes-conductor-deployment-6644848475-5k9vm", + outputData: { + response: { + headers: { + Server: ["Cowboy"], + Connection: ["keep-alive"], + "X-Powered-By": ["Express"], + "Content-Type": ["text/html; charset=utf-8"], + "Content-Length": ["41"], + Etag: ['W/"29-H58QMsCQTuEnY84zyDHEtcQuZTs"'], + Date: ["Mon, 06 Jun 2022 12:00:26 GMT"], + Via: ["1.1 vegur"], + }, + reasonPhrase: "OK", + body: "Success!13 widget(s) added to your order.", + statusCode: 200, + }, + }, + workflowTask: { + name: "reorder_widgets", + taskReferenceName: "reorder_widgets_ref", + inputParameters: { + http_request: { + uri: "http://restfuldemo.herokuapp.com/appendorder", + method: "POST", + body: { + item: "widget", + count: "${jq_sum_widgets_ref.output.result}", + }, + connectionTimeOut: 5000, + readTimeOut: 5000, + }, + asyncComplete: false, + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + taskDefinition: { + createTime: 1649342184551, + updateTime: 1649342255822, + createdBy: "", + updatedBy: "", + name: "reorder_widgets", + description: + "extending the reorder task to have 3 retries and fixed delay", + retryCount: 3, + timeoutSeconds: 10, + inputKeys: [], + outputKeys: [], + timeoutPolicy: "TIME_OUT_WF", + retryLogic: "FIXED", + retryDelaySeconds: 5, + responseTimeoutSeconds: 5, + inputTemplate: {}, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + ownerEmail: "doug.sillars@orkes.io", + backoffScaleFactor: 1, + }, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + retryCount: 3, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + taskDefinition: { + createTime: 1649342184551, + updateTime: 1649342255822, + createdBy: "", + updatedBy: "", + name: "reorder_widgets", + description: + "extending the reorder task to have 3 retries and fixed delay", + retryCount: 3, + timeoutSeconds: 10, + inputKeys: [], + outputKeys: [], + timeoutPolicy: "TIME_OUT_WF", + retryLogic: "FIXED", + retryDelaySeconds: 5, + responseTimeoutSeconds: 5, + inputTemplate: {}, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + ownerEmail: "doug.sillars@orkes.io", + backoffScaleFactor: 1, + }, + loopOverTask: false, + queueWaitTime: 39933090, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + case: "Success!13 widget(s) added to your order.", + }, + referenceTaskName: "switch_task", + retryCount: 0, + seq: 11, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1654516826196, + startTime: 1654516826196, + endTime: 1654516826212, + updateTime: 1654516826205, + startDelayInSeconds: 0, + retried: false, + executed: false, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "30dcf124-e590-11ec-99fe-ea3af9c7af2a", + workflowType: "Bobs_widget_fulfillment", + taskId: "408211ba-e590-11ec-99fe-ea3af9c7af2a", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["Success!13 widget(s) added to your order."], + }, + workflowTask: { + name: "order_checking", + taskReferenceName: "switch_task", + inputParameters: { + switchCaseValue: "${reorder_widgets_ref.output.response.body}", + }, + type: "SWITCH", + decisionCases: { + "Order failed.": [ + { + name: "terminate_fail", + taskReferenceName: "terminate_fail", + inputParameters: { + terminationStatus: "FAILED", + workflowOutput: { + orderDetails: + "${image_multiple_convert_resize_join_ref.output}", + reorder: "${reorder_widgets_ref.output.response.body}", + }, + }, + type: "TERMINATE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + }, + defaultCase: [ + { + name: "terminate_success", + taskReferenceName: "terminate_success", + inputParameters: { + terminationStatus: "COMPLETED", + workflowOutput: { + orderDetails: + "${image_multiple_convert_resize_join_ref.output}", + reorder: "${reorder_widgets_ref.output.response.body}", + }, + }, + type: "TERMINATE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + taskDefinition: null, + loopOverTask: false, + queueWaitTime: 0, + }, + { + taskType: "TERMINATE", + status: "COMPLETED", + inputData: { + terminationStatus: "COMPLETED", + workflowOutput: { + orderDetails: { + shipping_loop_subworkflow_ref_0: { + subWorkflowId: "31443e81-e590-11ec-99fe-ea3af9c7af2a", + orderDetails: { + 1: { + widget_shipping: { + fullAddress: + "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "81900100 68490647", + }, + }, + 2: { + widget_shipping: { + fullAddress: + "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "75954032 76216878", + }, + }, + 3: { + widget_shipping: { + fullAddress: + "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "52750497 19652062", + }, + }, + 4: { + widget_shipping: { + fullAddress: + "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "72896512 69020693", + }, + }, + 5: { + widget_shipping: { + fullAddress: + "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "61473582 94684741", + }, + }, + 6: { + widget_shipping: { + fullAddress: + "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "49293897 64849148", + }, + }, + 7: { + widget_shipping: { + fullAddress: + "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "72669959 93938409", + }, + }, + 8: { + widget_shipping: { + fullAddress: + "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "39018420 85980274", + }, + }, + 9: { + widget_shipping: { + fullAddress: + "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "8274183 9267380", + }, + }, + 10: { + widget_shipping: { + fullAddress: + "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "1884788 19309914", + }, + }, + 11: { + widget_shipping: { + fullAddress: + "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "29288059 71566372", + }, + }, + 12: { + widget_shipping: { + fullAddress: + "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "44861411 88039618", + }, + }, + iteration: 12, + }, + }, + shipping_loop_subworkflow_ref_1: { + subWorkflowId: "314bdfa4-e590-11ec-99fe-ea3af9c7af2a", + orderDetails: { + 1: { + widget_shipping: { + fullAddress: + "BobBobBob BobraAnn\n1 Surf Street\nKokomo, FL 53111", + trackingNumber: "5500341 47648420", + }, + }, + iteration: 1, + }, + }, + }, + reorder: "Success!13 widget(s) added to your order.", + }, + }, + referenceTaskName: "terminate_success", + retryCount: 0, + seq: 12, + pollCount: 0, + taskDefName: "terminate_success", + scheduledTime: 1654516826196, + startTime: 1654516826196, + endTime: 1654516826215, + updateTime: 1654516826207, + startDelayInSeconds: 0, + retried: false, + executed: false, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "30dcf124-e590-11ec-99fe-ea3af9c7af2a", + workflowType: "Bobs_widget_fulfillment", + taskId: "408211bb-e590-11ec-99fe-ea3af9c7af2a", + callbackAfterSeconds: 0, + outputData: { + orderDetails: { + shipping_loop_subworkflow_ref_0: { + subWorkflowId: "31443e81-e590-11ec-99fe-ea3af9c7af2a", + orderDetails: { + 1: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "81900100 68490647", + }, + }, + 2: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "75954032 76216878", + }, + }, + 3: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "52750497 19652062", + }, + }, + 4: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "72896512 69020693", + }, + }, + 5: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "61473582 94684741", + }, + }, + 6: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "49293897 64849148", + }, + }, + 7: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "72669959 93938409", + }, + }, + 8: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "39018420 85980274", + }, + }, + 9: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "8274183 9267380", + }, + }, + 10: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "1884788 19309914", + }, + }, + 11: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "29288059 71566372", + }, + }, + 12: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "44861411 88039618", + }, + }, + iteration: 12, + }, + }, + shipping_loop_subworkflow_ref_1: { + subWorkflowId: "314bdfa4-e590-11ec-99fe-ea3af9c7af2a", + orderDetails: { + 1: { + widget_shipping: { + fullAddress: + "BobBobBob BobraAnn\n1 Surf Street\nKokomo, FL 53111", + trackingNumber: "5500341 47648420", + }, + }, + iteration: 1, + }, + }, + }, + reorder: "Success!13 widget(s) added to your order.", + }, + workflowTask: { + name: "terminate_success", + taskReferenceName: "terminate_success", + inputParameters: { + terminationStatus: "COMPLETED", + workflowOutput: { + orderDetails: "${image_multiple_convert_resize_join_ref.output}", + reorder: "${reorder_widgets_ref.output.response.body}", + }, + }, + type: "TERMINATE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + taskDefinition: null, + loopOverTask: false, + queueWaitTime: 0, + }, + ], + input: { + _executedTime: 1654516799940, + _executionId: "42535504-8723-40cd-8303-05cbbecc3dd7", + _scheduledTime: 1654516800000, + addressList: [ + { + numberOfWidgets: "12", + name: "Bob McBobFace", + street: "21 Bob Lane", + city: "Bobville", + state: "OR", + zip: "53111", + }, + { + numberOfWidgets: "1", + name: "BobBobBob BobraAnn", + street: "1 Surf Street", + city: "Kokomo", + state: "FL", + zip: "53111", + }, + ], + _startedByScheduler: "doug_test", + }, + output: { + orderDetails: { + shipping_loop_subworkflow_ref_0: { + subWorkflowId: "31443e81-e590-11ec-99fe-ea3af9c7af2a", + orderDetails: { + 1: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "81900100 68490647", + }, + }, + 2: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "75954032 76216878", + }, + }, + 3: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "52750497 19652062", + }, + }, + 4: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "72896512 69020693", + }, + }, + 5: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "61473582 94684741", + }, + }, + 6: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "49293897 64849148", + }, + }, + 7: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "72669959 93938409", + }, + }, + 8: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "39018420 85980274", + }, + }, + 9: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "8274183 9267380", + }, + }, + 10: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "1884788 19309914", + }, + }, + 11: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "29288059 71566372", + }, + }, + 12: { + widget_shipping: { + fullAddress: "Bob McBobFace\n21 Bob Lane\nBobville, OR 53111", + trackingNumber: "44861411 88039618", + }, + }, + iteration: 12, + }, + }, + shipping_loop_subworkflow_ref_1: { + subWorkflowId: "314bdfa4-e590-11ec-99fe-ea3af9c7af2a", + orderDetails: { + 1: { + widget_shipping: { + fullAddress: + "BobBobBob BobraAnn\n1 Surf Street\nKokomo, FL 53111", + trackingNumber: "5500341 47648420", + }, + }, + iteration: 1, + }, + }, + }, + reorder: "Success!13 widget(s) added to your order.", + }, + reasonForIncompletion: + "Workflow is COMPLETED by TERMINATE task: 408211bb-e590-11ec-99fe-ea3af9c7af2a", + taskToDomain: {}, + failedReferenceTaskNames: ["reorder_widgets_ref"], + workflowDefinition: { + updateTime: 1649167373457, + name: "Bobs_widget_fulfillment", + description: "Shipping widgets right from Bob", + version: 4, + tasks: [ + { + name: "jq_address_count", + taskReferenceName: "jq_address_count_ref", + inputParameters: { + input: "${workflow.input.addressList}", + queryExpression: ".[] |length", + }, + type: "JSON_JQ_TRANSFORM", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "jq_create_dynamictasks", + taskReferenceName: "jq_create_dynamictasks_ref", + inputParameters: { + input: "{}", + queryExpression: + 'reduce range(0,${jq_address_count_ref.output.result}) as $f (.; .dynamicTasks[$f].subWorkflowParam.name = "Shipping_loop_workflow" | .dynamicTasks[$f].taskReferenceName = "shipping_loop_subworkflow_ref_\\($f)" | .dynamicTasks[$f].type = "SUB_WORKFLOW")', + }, + type: "JSON_JQ_TRANSFORM", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "jq_create_dynamictaskParams", + taskReferenceName: "jq_create_dynamictasksParams_ref", + inputParameters: { + input: "{}", + addresses: "${workflow.input.addressList}", + taskList: "${jq_create_dynamictasks_ref.output.result}", + queryExpression: + 'reduce range(0,${jq_address_count_ref.output.result}) as $f (.; .dynamicTasksInput."shipping_loop_subworkflow_ref_\\($f)" = .addresses[$f])', + }, + type: "JSON_JQ_TRANSFORM", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "shipping_dynamic_fork", + taskReferenceName: "shipping_dynamic_fork_ref", + inputParameters: { + dynamicTasks: + "${jq_create_dynamictasks_ref.output.result.dynamicTasks}", + dynamicTasksInput: + "${jq_create_dynamictasksParams_ref.output.result.dynamicTasksInput}", + }, + type: "FORK_JOIN_DYNAMIC", + decisionCases: {}, + dynamicForkTasksParam: "dynamicTasks", + dynamicForkTasksInputParamName: "dynamicTasksInput", + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "shipping_multiple_addresses_join", + taskReferenceName: "image_multiple_convert_resize_join_ref", + inputParameters: {}, + type: "JOIN", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "jq_sum_widgets", + taskReferenceName: "jq_sum_widgets_ref", + inputParameters: { + input: "${workflow.input.addressList}", + queryExpression: "[.input[].numberOfWidgets | tonumber ] | add", + }, + type: "JSON_JQ_TRANSFORM", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "reorder_widgets", + taskReferenceName: "reorder_widgets_ref", + inputParameters: { + http_request: { + uri: "http://restfuldemo.herokuapp.com/appendorder", + method: "POST", + body: { + item: "widget", + count: "${jq_sum_widgets_ref.output.result}", + }, + connectionTimeOut: 5000, + readTimeOut: 5000, + }, + asyncComplete: false, + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + taskDefinition: { + createTime: 1649342184551, + updateTime: 1649342255822, + createdBy: "", + updatedBy: "", + name: "reorder_widgets", + description: + "extending the reorder task to have 3 retries and fixed delay", + retryCount: 3, + timeoutSeconds: 10, + inputKeys: [], + outputKeys: [], + timeoutPolicy: "TIME_OUT_WF", + retryLogic: "FIXED", + retryDelaySeconds: 5, + responseTimeoutSeconds: 5, + inputTemplate: {}, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + ownerEmail: "doug.sillars@orkes.io", + backoffScaleFactor: 1, + }, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + retryCount: 3, + }, + { + name: "order_checking", + taskReferenceName: "switch_task", + inputParameters: { + switchCaseValue: "${reorder_widgets_ref.output.response.body}", + }, + type: "SWITCH", + decisionCases: { + "Order failed.": [ + { + name: "terminate_fail", + taskReferenceName: "terminate_fail", + inputParameters: { + terminationStatus: "FAILED", + workflowOutput: { + orderDetails: + "${image_multiple_convert_resize_join_ref.output}", + reorder: "${reorder_widgets_ref.output.response.body}", + }, + }, + type: "TERMINATE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + }, + defaultCase: [ + { + name: "terminate_success", + taskReferenceName: "terminate_success", + inputParameters: { + terminationStatus: "COMPLETED", + workflowOutput: { + orderDetails: + "${image_multiple_convert_resize_join_ref.output}", + reorder: "${reorder_widgets_ref.output.response.body}", + }, + }, + type: "TERMINATE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + }, + ], + inputParameters: [], + outputParameters: { + orderDetails: "${image_multiple_convert_resize_join_ref.output}", + reorder: "${reorder_widgets_ref.output.response.body}", + }, + failureWorkflow: "shipping_failure", + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: false, + ownerEmail: "bob@bobswidgets.com", + timeoutPolicy: "ALERT_ONLY", + timeoutSeconds: 0, + failureWorkflow: "", + variables: {}, + inputTemplate: {}, + }, + priority: 0, + variables: {}, + lastRetriedTime: 0, + workflowName: "Bobs_widget_fulfillment", + workflowVersion: 4, + startTime: 1654516799948, +}; + +export const sampleWithDynamicFork = { + createTime: 1658878164317, + createdBy: "reumontf@gmail.com", + updatedBy: "", + status: "COMPLETED", + endTime: 1658878165230, + workflowId: "c89a41a7-0d3a-11ed-8160-da9da95e170f", + parentWorkflowId: "", + parentWorkflowTaskId: "", + tasks: [ + /* { */ + /* taskType: "TASK_SUMMARY", */ + /* status: "COMPLETED", */ + /* referenceTaskName: "fork_ref", */ + /* retryCount: 0, */ + /* seq: 1, */ + /* pollCount: 0, */ + /* taskDefName: "FORK", */ + /* scheduledTime: 1658878164323, */ + /* startTime: 1658878164323, */ + /* endTime: 1658878164323, */ + /* updateTime: 1658878164328, */ + /* startDelayInSeconds: 0, */ + /* retried: false, */ + /* executed: true, */ + /* callbackFromWorker: true, */ + /* responseTimeoutSeconds: 0, */ + /* workflowInstanceId: "c89a41a7-0d3a-11ed-8160-da9da95e170f", */ + /* workflowType: "dynamic_fork", */ + /* taskId: "c89b04f8-0d3a-11ed-8160-da9da95e170f", */ + /* callbackAfterSeconds: 0, */ + /* outputData: { */ + /* summary:{ */ + /* totalTasks:2000, */ + /* taskCountByStatus:{ */ + /* IN_PROGRESS:29800, */ + /* COMPLETED:15000, */ + /* SCHEDULED:2000, */ + /* FAILED:20 */ + /* } */ + /* } */ + /* }, */ + /* workflowTask: { */ + /* name: "fork", */ + /* taskReferenceName: "fork_ref", */ + /* inputParameters: { */ + /* dynamicTasks: "${workflow.input.dynamicTasks}", */ + /* dynamicTasksInput: "${workflow.input.dynamicTasksInputs}", */ + /* }, */ + /* type: "FORK_JOIN_DYNAMIC", */ + /* decisionCases: {}, */ + /* dynamicForkTasksParam: "dynamicTasks", */ + /* dynamicForkTasksInputParamName: "dynamicTasksInput", */ + /* defaultCase: [], */ + /* forkTasks: [], */ + /* startDelay: 0, */ + /* joinOn: [], */ + /* optional: false, */ + /* defaultExclusiveJoinTask: [], */ + /* asyncComplete: false, */ + /* loopOver: [], */ + /* }, */ + /* rateLimitPerFrequency: 0, */ + /* rateLimitFrequencyInSeconds: 0, */ + /* workflowPriority: 0, */ + /* iteration: 0, */ + /* subworkflowChanged: false, */ + /* queueWaitTime: 0, */ + /* loopOverTask: false, */ + /* taskDefinition: null, */ + /* }, */ + + { + taskType: "FORK", + status: "COMPLETED", + inputData: { + forkedTaskDefs: [ + { + asyncComplete: false, + joinOn: [], + optional: false, + type: "HTTP", + inputParameters: { + asyncComplete: false, + http_request: { + method: "GET", + readTimeOut: 3000, + uri: "https://catfact.ninja/fact", + connectionTimeOut: 3000, + }, + }, + decisionCases: {}, + loopOver: [], + name: "get_random_fact", + startDelay: 0, + defaultExclusiveJoinTask: [], + taskReferenceName: "get_random_fact_0", + defaultCase: [], + forkTasks: [], + }, + { + asyncComplete: false, + joinOn: [], + optional: false, + type: "HTTP", + inputParameters: { + asyncComplete: false, + http_request: { + method: "GET", + readTimeOut: 3000, + uri: "https://catfact.ninja/fact", + connectionTimeOut: 3000, + }, + }, + decisionCases: {}, + loopOver: [], + name: "get_random_fact", + startDelay: 0, + defaultExclusiveJoinTask: [], + taskReferenceName: "get_random_fact_1", + defaultCase: [], + forkTasks: [], + }, + ], + forkedTasks: ["get_random_fact_0", "get_random_fact_1"], + }, + referenceTaskName: "fork_ref", + retryCount: 0, + seq: 1, + pollCount: 0, + taskDefName: "FORK", + scheduledTime: 1658878164323, + startTime: 1658878164323, + endTime: 1658878164323, + updateTime: 1658878164328, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "c89a41a7-0d3a-11ed-8160-da9da95e170f", + workflowType: "dynamic_fork", + taskId: "c89b04f8-0d3a-11ed-8160-da9da95e170f", + callbackAfterSeconds: 0, + outputData: {}, + workflowTask: { + name: "fork", + taskReferenceName: "fork_ref", + inputParameters: { + dynamicTasks: "${workflow.input.dynamicTasks}", + dynamicTasksInput: "${workflow.input.dynamicTasksInputs}", + }, + type: "FORK_JOIN_DYNAMIC", + decisionCases: {}, + dynamicForkTasksParam: "dynamicTasks", + dynamicForkTasksInputParamName: "dynamicTasksInput", + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: false, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + asyncComplete: false, + http_request: { + method: "GET", + readTimeOut: 3000, + uri: "https://catfact.ninja/fact", + connectionTimeOut: 3000, + }, + }, + referenceTaskName: "get_random_fact_0", + retryCount: 0, + seq: 2, + pollCount: 1, + taskDefName: "get_random_fact", + scheduledTime: 1658878164323, + startTime: 1658878164941, + endTime: 1658878165092, + updateTime: 1658878164975, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "c89a41a7-0d3a-11ed-8160-da9da95e170f", + workflowType: "dynamic_fork", + taskId: "c89b2c09-0d3a-11ed-8160-da9da95e170f", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7f568bbff9-jldbm", + outputData: { + response: { + headers: { + "Transfer-Encoding": ["chunked"], + Server: ["nginx"], + "X-Ratelimit-Remaining": ["99"], + "Access-Control-Allow-Origin": ["*"], + "X-Content-Type-Options": ["nosniff"], + Connection: ["keep-alive"], + Date: ["Tue, 26 Jul 2022 23:29:25 GMT"], + "X-Frame-Options": ["SAMEORIGIN"], + "X-Ratelimit-Limit": ["100"], + "Cache-Control": ["no-cache, private"], + Vary: ["Accept-Encoding"], + "Set-Cookie": [ + "XSRF-TOKEN=eyJpdiI6IkNoM1FlOEZubkNwRStwTGVzdG9NR1E9PSIsInZhbHVlIjoiYlU3QXBycXBYRFZlcGQ2dVFjNmUvWmxuMGVIZ1VkR3YrNjVzSGUyVUFFQTdpbnV2cmpQbmRjVGlyQ09DUmRUUFBGaWZKaTNrMnBaS0syUERZQUVnckVHUFBjYVdZN3F5NDgwU2lTdTdxVnlMVVY4ZHYvd3JxcVlUdFlkSG1zK2ciLCJtYWMiOiI5NzlmYjJmMjk4ZGZjYjc2MGE1ZjU1OWY3MGRmODIwZWQxZDg4YTIyODk4NWNhNGZiOWEwZTFlNTA5MjkzOWE0IiwidGFnIjoiIn0%3D; expires=Wed, 27-Jul-2022 01:29:25 GMT; path=/; samesite=lax", + "cat_facts_session=eyJpdiI6Ikc5V0lkQk4wUDQ5eSsxTXlJWXQwRXc9PSIsInZhbHVlIjoiUHhwUDNZMU0zV2ZTQ25PN0FXYmhoUFNSYmwyTXhGN1lOYlhReCtNc2xweVF0RWRyZy80RTZVUmhLUldwOW9DVmozUEFMWnIvbS9vbDJBcHlYMEh0NmowY0lGbi94VFczejZpSEpaUFRpR1kyMnZyL0RwVG1COEk1aklneFBYdG0iLCJtYWMiOiIzNDYwYzUxZjVlY2UyZWEyM2I0ZmE2ZWY2YWM0NTZhOTA3YzFmYWFmNmYzOGUxMGU2ZWYxNDRmODUwMzk3MzZjIiwidGFnIjoiIn0%3D; expires=Wed, 27-Jul-2022 01:29:25 GMT; path=/; httponly; samesite=lax", + ], + "X-XSS-Protection": ["1; mode=block"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + fact: "A cat?s heart beats nearly twice as fast as a human heart, at 110 to 140 beats a minute.", + length: 88, + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "get_random_fact", + taskReferenceName: "get_random_fact_0", + inputParameters: {}, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + queueWaitTime: 618, + loopOverTask: false, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + asyncComplete: false, + http_request: { + method: "GET", + readTimeOut: 3000, + uri: "https://catfact.ninja/fact", + connectionTimeOut: 3000, + }, + }, + referenceTaskName: "get_random_fact_1", + retryCount: 0, + seq: 3, + pollCount: 1, + taskDefName: "get_random_fact", + scheduledTime: 1658878164323, + startTime: 1658878164974, + endTime: 1658878165087, + updateTime: 1658878164974, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "c89a41a7-0d3a-11ed-8160-da9da95e170f", + workflowType: "dynamic_fork", + taskId: "c89b2c0a-0d3a-11ed-8160-da9da95e170f", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7f568bbff9-hgn6k", + outputData: { + response: { + headers: { + "Transfer-Encoding": ["chunked"], + Server: ["nginx"], + "X-Ratelimit-Remaining": ["99"], + "Access-Control-Allow-Origin": ["*"], + "X-Content-Type-Options": ["nosniff"], + Connection: ["keep-alive"], + Date: ["Tue, 26 Jul 2022 23:29:25 GMT"], + "X-Frame-Options": ["SAMEORIGIN"], + "X-Ratelimit-Limit": ["100"], + "Cache-Control": ["no-cache, private"], + Vary: ["Accept-Encoding"], + "Set-Cookie": [ + "XSRF-TOKEN=eyJpdiI6IlVxSGNSb1R4eVVqL1EvRzJuZVBGMnc9PSIsInZhbHVlIjoiMGVkNmZMOWg5aEJiZlloNEl3aTNPQTRkM1k1b0tScG1Xb2c1NzEzTTZDRDN1QWFGVDV4YzFLT21nNzZMYUZmTC9ld0Q1Zk5wbUY1NWZKcUI3cVltRDdGV3VMV0NvYjQwdkliNVI5b0Zaek8xejBnalNYMkhPUEk2ek1ja1Z1cFEiLCJtYWMiOiI4YWQ2YjYxZDZkMWY0MjI5NWI1Mjg2ODEyOWQxYmUzZjEzY2U0NzE3N2FlMzg3NDNiOWYxZDAwOTNkMzQxODNlIiwidGFnIjoiIn0%3D; expires=Wed, 27-Jul-2022 01:29:25 GMT; path=/; samesite=lax", + "cat_facts_session=eyJpdiI6IlN6dEQ3MW9lL0IxWmZsb3E0MHR3ZHc9PSIsInZhbHVlIjoidVRUK3EzS0kzT05mT1d0V01HU3FsS2ZlVy9EajFzTWRmM2M2ajhSSDV2aVVzY21mYzJJdTJ3cnRPSjZPclVkSW9sUEMvaDE3V05OMmFWZEIzQ0w4MFBKVTBhWEFpNXp4a20wdVNHRlY4dDAyOTFwVEI4cTBHRHN2VmEwenNhMDAiLCJtYWMiOiJkZmEwMGQ0ZWY5ODk3YjE2ZDM2NDkwYTE2ZGJhODBiNTUzMTk4ODA2OGZmN2MzZjBlOWRiZWE4YjM3YWNlYTU2IiwidGFnIjoiIn0%3D; expires=Wed, 27-Jul-2022 01:29:25 GMT; path=/; httponly; samesite=lax", + ], + "X-XSS-Protection": ["1; mode=block"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + fact: "A cat has more bones than a human being; humans have 206 and the cat has 230 bones.", + length: 83, + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "get_random_fact", + taskReferenceName: "get_random_fact_1", + inputParameters: {}, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + queueWaitTime: 651, + loopOverTask: false, + taskDefinition: null, + }, + { + taskType: "JOIN", + status: "COMPLETED", + inputData: { joinOn: ["get_random_fact_0", "get_random_fact_1"] }, + referenceTaskName: "join_ref", + retryCount: 0, + seq: 4, + pollCount: 0, + taskDefName: "JOIN", + scheduledTime: 1658878164323, + startTime: 1658878164323, + endTime: 1658878165179, + updateTime: 1658878164331, + startDelayInSeconds: 0, + retried: false, + executed: false, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "c89a41a7-0d3a-11ed-8160-da9da95e170f", + workflowType: "dynamic_fork", + taskId: "c89b2c0b-0d3a-11ed-8160-da9da95e170f", + callbackAfterSeconds: 0, + outputData: { + get_random_fact_0: { + response: { + headers: { + "Transfer-Encoding": ["chunked"], + Server: ["nginx"], + "X-Ratelimit-Remaining": ["99"], + "Access-Control-Allow-Origin": ["*"], + "X-Content-Type-Options": ["nosniff"], + Connection: ["keep-alive"], + Date: ["Tue, 26 Jul 2022 23:29:25 GMT"], + "X-Frame-Options": ["SAMEORIGIN"], + "X-Ratelimit-Limit": ["100"], + "Cache-Control": ["no-cache, private"], + Vary: ["Accept-Encoding"], + "Set-Cookie": [ + "XSRF-TOKEN=eyJpdiI6IkNoM1FlOEZubkNwRStwTGVzdG9NR1E9PSIsInZhbHVlIjoiYlU3QXBycXBYRFZlcGQ2dVFjNmUvWmxuMGVIZ1VkR3YrNjVzSGUyVUFFQTdpbnV2cmpQbmRjVGlyQ09DUmRUUFBGaWZKaTNrMnBaS0syUERZQUVnckVHUFBjYVdZN3F5NDgwU2lTdTdxVnlMVVY4ZHYvd3JxcVlUdFlkSG1zK2ciLCJtYWMiOiI5NzlmYjJmMjk4ZGZjYjc2MGE1ZjU1OWY3MGRmODIwZWQxZDg4YTIyODk4NWNhNGZiOWEwZTFlNTA5MjkzOWE0IiwidGFnIjoiIn0%3D; expires=Wed, 27-Jul-2022 01:29:25 GMT; path=/; samesite=lax", + "cat_facts_session=eyJpdiI6Ikc5V0lkQk4wUDQ5eSsxTXlJWXQwRXc9PSIsInZhbHVlIjoiUHhwUDNZMU0zV2ZTQ25PN0FXYmhoUFNSYmwyTXhGN1lOYlhReCtNc2xweVF0RWRyZy80RTZVUmhLUldwOW9DVmozUEFMWnIvbS9vbDJBcHlYMEh0NmowY0lGbi94VFczejZpSEpaUFRpR1kyMnZyL0RwVG1COEk1aklneFBYdG0iLCJtYWMiOiIzNDYwYzUxZjVlY2UyZWEyM2I0ZmE2ZWY2YWM0NTZhOTA3YzFmYWFmNmYzOGUxMGU2ZWYxNDRmODUwMzk3MzZjIiwidGFnIjoiIn0%3D; expires=Wed, 27-Jul-2022 01:29:25 GMT; path=/; httponly; samesite=lax", + ], + "X-XSS-Protection": ["1; mode=block"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + fact: "A cat?s heart beats nearly twice as fast as a human heart, at 110 to 140 beats a minute.", + length: 88, + }, + statusCode: 200, + }, + }, + get_random_fact_1: { + response: { + headers: { + "Transfer-Encoding": ["chunked"], + Server: ["nginx"], + "X-Ratelimit-Remaining": ["99"], + "Access-Control-Allow-Origin": ["*"], + "X-Content-Type-Options": ["nosniff"], + Connection: ["keep-alive"], + Date: ["Tue, 26 Jul 2022 23:29:25 GMT"], + "X-Frame-Options": ["SAMEORIGIN"], + "X-Ratelimit-Limit": ["100"], + "Cache-Control": ["no-cache, private"], + Vary: ["Accept-Encoding"], + "Set-Cookie": [ + "XSRF-TOKEN=eyJpdiI6IlVxSGNSb1R4eVVqL1EvRzJuZVBGMnc9PSIsInZhbHVlIjoiMGVkNmZMOWg5aEJiZlloNEl3aTNPQTRkM1k1b0tScG1Xb2c1NzEzTTZDRDN1QWFGVDV4YzFLT21nNzZMYUZmTC9ld0Q1Zk5wbUY1NWZKcUI3cVltRDdGV3VMV0NvYjQwdkliNVI5b0Zaek8xejBnalNYMkhPUEk2ek1ja1Z1cFEiLCJtYWMiOiI4YWQ2YjYxZDZkMWY0MjI5NWI1Mjg2ODEyOWQxYmUzZjEzY2U0NzE3N2FlMzg3NDNiOWYxZDAwOTNkMzQxODNlIiwidGFnIjoiIn0%3D; expires=Wed, 27-Jul-2022 01:29:25 GMT; path=/; samesite=lax", + "cat_facts_session=eyJpdiI6IlN6dEQ3MW9lL0IxWmZsb3E0MHR3ZHc9PSIsInZhbHVlIjoidVRUK3EzS0kzT05mT1d0V01HU3FsS2ZlVy9EajFzTWRmM2M2ajhSSDV2aVVzY21mYzJJdTJ3cnRPSjZPclVkSW9sUEMvaDE3V05OMmFWZEIzQ0w4MFBKVTBhWEFpNXp4a20wdVNHRlY4dDAyOTFwVEI4cTBHRHN2VmEwenNhMDAiLCJtYWMiOiJkZmEwMGQ0ZWY5ODk3YjE2ZDM2NDkwYTE2ZGJhODBiNTUzMTk4ODA2OGZmN2MzZjBlOWRiZWE4YjM3YWNlYTU2IiwidGFnIjoiIn0%3D; expires=Wed, 27-Jul-2022 01:29:25 GMT; path=/; httponly; samesite=lax", + ], + "X-XSS-Protection": ["1; mode=block"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + fact: "A cat has more bones than a human being; humans have 206 and the cat has 230 bones.", + length: 83, + }, + statusCode: 200, + }, + }, + }, + workflowTask: { + name: "join", + taskReferenceName: "join_ref", + inputParameters: {}, + type: "JOIN", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: false, + taskDefinition: null, + }, + ], + input: { + dynamicTasksInputs: { get_random_fact_0: {}, get_random_fact_1: {} }, + dynamicTasks: [ + { + name: "get_random_fact", + taskReferenceName: "get_random_fact_0", + type: "HTTP", + inputParameters: { + http_request: { + method: "GET", + readTimeOut: 3000, + uri: "https://catfact.ninja/fact", + connectionTimeOut: 3000, + }, + }, + }, + { + name: "get_random_fact", + taskReferenceName: "get_random_fact_1", + type: "HTTP", + inputParameters: { + http_request: { + method: "GET", + readTimeOut: 3000, + uri: "https://catfact.ninja/fact", + connectionTimeOut: 3000, + }, + }, + }, + ], + }, + output: { + output: { + get_random_fact_0: { + response: { + headers: { + "Transfer-Encoding": ["chunked"], + Server: ["nginx"], + "X-Ratelimit-Remaining": ["99"], + "Access-Control-Allow-Origin": ["*"], + "X-Content-Type-Options": ["nosniff"], + Connection: ["keep-alive"], + Date: ["Tue, 26 Jul 2022 23:29:25 GMT"], + "X-Frame-Options": ["SAMEORIGIN"], + "X-Ratelimit-Limit": ["100"], + "Cache-Control": ["no-cache, private"], + Vary: ["Accept-Encoding"], + "Set-Cookie": [ + "XSRF-TOKEN=eyJpdiI6IkNoM1FlOEZubkNwRStwTGVzdG9NR1E9PSIsInZhbHVlIjoiYlU3QXBycXBYRFZlcGQ2dVFjNmUvWmxuMGVIZ1VkR3YrNjVzSGUyVUFFQTdpbnV2cmpQbmRjVGlyQ09DUmRUUFBGaWZKaTNrMnBaS0syUERZQUVnckVHUFBjYVdZN3F5NDgwU2lTdTdxVnlMVVY4ZHYvd3JxcVlUdFlkSG1zK2ciLCJtYWMiOiI5NzlmYjJmMjk4ZGZjYjc2MGE1ZjU1OWY3MGRmODIwZWQxZDg4YTIyODk4NWNhNGZiOWEwZTFlNTA5MjkzOWE0IiwidGFnIjoiIn0%3D; expires=Wed, 27-Jul-2022 01:29:25 GMT; path=/; samesite=lax", + "cat_facts_session=eyJpdiI6Ikc5V0lkQk4wUDQ5eSsxTXlJWXQwRXc9PSIsInZhbHVlIjoiUHhwUDNZMU0zV2ZTQ25PN0FXYmhoUFNSYmwyTXhGN1lOYlhReCtNc2xweVF0RWRyZy80RTZVUmhLUldwOW9DVmozUEFMWnIvbS9vbDJBcHlYMEh0NmowY0lGbi94VFczejZpSEpaUFRpR1kyMnZyL0RwVG1COEk1aklneFBYdG0iLCJtYWMiOiIzNDYwYzUxZjVlY2UyZWEyM2I0ZmE2ZWY2YWM0NTZhOTA3YzFmYWFmNmYzOGUxMGU2ZWYxNDRmODUwMzk3MzZjIiwidGFnIjoiIn0%3D; expires=Wed, 27-Jul-2022 01:29:25 GMT; path=/; httponly; samesite=lax", + ], + "X-XSS-Protection": ["1; mode=block"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + fact: "A cat?s heart beats nearly twice as fast as a human heart, at 110 to 140 beats a minute.", + length: 88, + }, + statusCode: 200, + }, + }, + get_random_fact_1: { + response: { + headers: { + "Transfer-Encoding": ["chunked"], + Server: ["nginx"], + "X-Ratelimit-Remaining": ["99"], + "Access-Control-Allow-Origin": ["*"], + "X-Content-Type-Options": ["nosniff"], + Connection: ["keep-alive"], + Date: ["Tue, 26 Jul 2022 23:29:25 GMT"], + "X-Frame-Options": ["SAMEORIGIN"], + "X-Ratelimit-Limit": ["100"], + "Cache-Control": ["no-cache, private"], + Vary: ["Accept-Encoding"], + "Set-Cookie": [ + "XSRF-TOKEN=eyJpdiI6IlVxSGNSb1R4eVVqL1EvRzJuZVBGMnc9PSIsInZhbHVlIjoiMGVkNmZMOWg5aEJiZlloNEl3aTNPQTRkM1k1b0tScG1Xb2c1NzEzTTZDRDN1QWFGVDV4YzFLT21nNzZMYUZmTC9ld0Q1Zk5wbUY1NWZKcUI3cVltRDdGV3VMV0NvYjQwdkliNVI5b0Zaek8xejBnalNYMkhPUEk2ek1ja1Z1cFEiLCJtYWMiOiI4YWQ2YjYxZDZkMWY0MjI5NWI1Mjg2ODEyOWQxYmUzZjEzY2U0NzE3N2FlMzg3NDNiOWYxZDAwOTNkMzQxODNlIiwidGFnIjoiIn0%3D; expires=Wed, 27-Jul-2022 01:29:25 GMT; path=/; samesite=lax", + "cat_facts_session=eyJpdiI6IlN6dEQ3MW9lL0IxWmZsb3E0MHR3ZHc9PSIsInZhbHVlIjoidVRUK3EzS0kzT05mT1d0V01HU3FsS2ZlVy9EajFzTWRmM2M2ajhSSDV2aVVzY21mYzJJdTJ3cnRPSjZPclVkSW9sUEMvaDE3V05OMmFWZEIzQ0w4MFBKVTBhWEFpNXp4a20wdVNHRlY4dDAyOTFwVEI4cTBHRHN2VmEwenNhMDAiLCJtYWMiOiJkZmEwMGQ0ZWY5ODk3YjE2ZDM2NDkwYTE2ZGJhODBiNTUzMTk4ODA2OGZmN2MzZjBlOWRiZWE4YjM3YWNlYTU2IiwidGFnIjoiIn0%3D; expires=Wed, 27-Jul-2022 01:29:25 GMT; path=/; httponly; samesite=lax", + ], + "X-XSS-Protection": ["1; mode=block"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + fact: "A cat has more bones than a human being; humans have 206 and the cat has 230 bones.", + length: 83, + }, + statusCode: 200, + }, + }, + }, + }, + taskToDomain: {}, + failedReferenceTaskNames: [], + workflowDefinition: { + createTime: 0, + updateTime: 0, + name: "dynamic_fork", + description: "dynamic fork join example", + version: 1, + tasks: [ + { + name: "fork", + taskReferenceName: "fork_ref", + inputParameters: { + dynamicTasks: "${workflow.input.dynamicTasks}", + dynamicTasksInput: "${workflow.input.dynamicTasksInputs}", + }, + type: "FORK_JOIN_DYNAMIC", + decisionCases: {}, + dynamicForkTasksParam: "dynamicTasks", + dynamicForkTasksInputParamName: "dynamicTasksInput", + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "join", + taskReferenceName: "join_ref", + inputParameters: {}, + type: "JOIN", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + inputParameters: ["dynamicTasks", "dynamicTasksInputs"], + outputParameters: { output: "${join_ref.output}" }, + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: false, + ownerEmail: "reumontf@gmail.com", + timeoutPolicy: "ALERT_ONLY", + timeoutSeconds: 0, + failureWorkflow: "", + variables: {}, + inputTemplate: {}, + }, + priority: 0, + variables: {}, + lastRetriedTime: 0, + startTime: 1658878164317, + workflowVersion: 1, + workflowName: "dynamic_fork", +}; + +export const newDynamicForkSample = { + createTime: 1686804244860, + updateTime: 1686803821513, + name: "najeeb_15_june_fork", + description: + "Edit or extend this sample workflow. Set the workflow name to get started", + version: 1, + tasks: [ + { + name: "dynamic", + taskReferenceName: "dynamic_ref", + inputParameters: { + dynamicTasks: [ + { + name: "image_convert_resize", + taskReferenceName: "image_convert_resize_png_300x300_0", + }, + { + name: "image_convert_resize", + taskReferenceName: "image_convert_resize_png_200x200_1", + }, + { + name: "check", + taskReferenceName: "fallsas", + }, + ], + dynamicTasksInput: { + image_convert_resize_png_300x300_0: { + outputWidth: 300, + outputHeight: 300, + }, + image_convert_resize_png_200x200_1: { + outputWidth: 200, + outputHeight: 200, + }, + }, + }, + type: "FORK_JOIN_DYNAMIC", + decisionCases: {}, + dynamicForkTasksParam: "dynamicTasks", + dynamicForkTasksInputParamName: "dynamicTasksInput", + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "join_task", + taskReferenceName: "join_task_ref", + inputParameters: {}, + type: "JOIN", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + inputParameters: [], + outputParameters: { + data: "${get_random_fact.output.response.body.fact}", + }, + failureWorkflow: "", + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: false, + ownerEmail: "najeeb.thangal@orkes.io", + timeoutPolicy: "ALERT_ONLY", + timeoutSeconds: 0, + variables: {}, + inputTemplate: {}, + onStateChange: {}, +}; + +export const sampleExecutionDoWhile = { + ownerApp: "nhandt+006@orkes.io", + createTime: 1711431102670, + updateTime: 1711431104863, + createdBy: "najeeb.thangal@orkes.io", + updatedBy: "", + status: "COMPLETED", + endTime: 1711431104861, + workflowId: "20d3b134-eb32-11ee-8b0a-0e6876359850", + parentWorkflowId: "", + parentWorkflowTaskId: "", + tasks: [ + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_first", + retryCount: 0, + seq: 1, + pollCount: 1, + taskDefName: "http_first", + scheduledTime: 1711431102672, + startTime: 1711431102750, + endTime: 1711431102811, + updateTime: 1711431102750, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "20d3b134-eb32-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test", + taskId: "20d3ff55-eb32-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Tue, 26 Mar 2024 05:31:42 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 339, + hostName: "orkes-api-sampler-67dfc8cf58-jk6kd", + randomString: "mxxsreljbjqgqwojkqxd", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_first", + taskReferenceName: "http_ref_first", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + loopOverTask: false, + taskDefinition: null, + queueWaitTime: 78, + }, + { + taskType: "DO_WHILE", + status: "COMPLETED", + inputData: { + number: 10, + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "do_while_ref", + retryCount: 0, + seq: 2, + pollCount: 0, + taskDefName: "do_while", + scheduledTime: 1711431102815, + startTime: 1711431102815, + endTime: 1711431104753, + updateTime: 1711431104550, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "20d3b134-eb32-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test", + taskId: "20e9aa36-eb32-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + outputData: { + 1: { + http_ref_cool: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 05:31:42 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4701, + hostName: "orkes-api-sampler-67dfc8cf58-ktrql", + randomString: "zflljulcusapgwfaiytx", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + switch_ref: { + evaluationResult: ["2"], + selectedCase: "2", + }, + http_third_ref: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Tue, 26 Mar 2024 05:31:42 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 745, + hostName: "orkes-api-sampler-67dfc8cf58-dcsmz", + randomString: "vblryduefgemnwrifggg", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 2: { + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_second_ref: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 05:31:43 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 6714, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "gspbuyqidhyripmblnhh", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_four_ref: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 05:31:43 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 3391, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "qoauwnmkptdtwonrallz", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 3: { + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_second_ref: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Tue, 26 Mar 2024 05:31:43 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 591, + hostName: "orkes-api-sampler-67dfc8cf58-mzb8h", + randomString: "osikimmicsohimctzynf", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_four_ref: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 05:31:43 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9953, + hostName: "orkes-api-sampler-67dfc8cf58-7l8kb", + randomString: "mfdhzhvuammftfncjduz", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 4: { + switch_ref: { + evaluationResult: ["4"], + selectedCase: "4", + }, + http_second_ref: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 05:31:43 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7297, + hostName: "orkes-api-sampler-67dfc8cf58-jk6kd", + randomString: "emnrcqiwfbrkybhsyptc", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_four_ref: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 05:31:43 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 6651, + hostName: "orkes-api-sampler-67dfc8cf58-dcsmz", + randomString: "uctrykkkcchadijylujt", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 5: { + http_ref_five: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 05:31:43 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 1775, + hostName: "orkes-api-sampler-67dfc8cf58-xsh5s", + randomString: "uscnqmblxmnegjlnbtaa", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + switch_ref: { + evaluationResult: ["3"], + selectedCase: "3", + }, + }, + 6: { + http_ref_five: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 05:31:43 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9639, + hostName: "orkes-api-sampler-67dfc8cf58-ktrql", + randomString: "zwbfptybsvekbfvzbzml", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + switch_ref: { + evaluationResult: ["3"], + selectedCase: "3", + }, + }, + 7: { + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_second_ref: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 05:31:43 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2929, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "ksityuwtdupeziewwqyh", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_four_ref: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 05:31:44 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 1547, + hostName: "orkes-api-sampler-67dfc8cf58-mzb8h", + randomString: "xlttyspawtakwfzgslan", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 8: { + http_ref_cool: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Tue, 26 Mar 2024 05:31:44 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 363, + hostName: "orkes-api-sampler-67dfc8cf58-jk6kd", + randomString: "hhlhczvqswurlwzjclyi", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + switch_ref: { + evaluationResult: ["2"], + selectedCase: "2", + }, + http_third_ref: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 05:31:44 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5290, + hostName: "orkes-api-sampler-67dfc8cf58-sts78", + randomString: "ucbkmlxiyqnyakiqggrj", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 9: { + switch_ref: { + evaluationResult: ["4"], + selectedCase: "4", + }, + http_second_ref: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 05:31:44 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5292, + hostName: "orkes-api-sampler-67dfc8cf58-dcsmz", + randomString: "zejbepozxwbqfguyhkal", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_four_ref: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Tue, 26 Mar 2024 05:31:44 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 234, + hostName: "orkes-api-sampler-67dfc8cf58-xsh5s", + randomString: "ynvpazmgmmwgqjxnilxo", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 10: { + switch_ref: { + evaluationResult: ["4"], + selectedCase: "4", + }, + http_second_ref: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 05:31:44 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7926, + hostName: "orkes-api-sampler-67dfc8cf58-ktrql", + randomString: "rzcxmijtfvpcvxqrtblg", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_four_ref: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 05:31:44 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5867, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "lztsmmtdznjtgbjialsw", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + iteration: 10, + }, + workflowTask: { + name: "do_while", + taskReferenceName: "do_while_ref", + inputParameters: { + number: 10, + }, + type: "DO_WHILE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopCondition: + "(function () {\nif ($.do_while_ref['iteration'] < $.number) {\nreturn true;\n}\nreturn false;\n})();", + loopOver: [ + { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + "": "", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_third", + taskReferenceName: "http_third_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_cool", + taskReferenceName: "http_ref_cool", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 3: [ + { + name: "http_five", + taskReferenceName: "http_ref_five", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_second", + taskReferenceName: "http_second_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_four", + taskReferenceName: "http_ref_four_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: + "(function () {\n const number = Math.floor(Math.random() * (4 - 1 + 1)) + 1;\n return number;\n }())", + onStateChange: {}, + }, + ], + evaluatorType: "graaljs", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + loopOverTask: true, + taskDefinition: null, + queueWaitTime: 0, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + "": "", + hasChildren: "true", + _createdBy: "najeeb.thangal@orkes.io", + case: "2", + }, + referenceTaskName: "switch_ref__1", + retryCount: 0, + seq: 3, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1711431102833, + startTime: 1711431102833, + endTime: 1711431102833, + updateTime: 1711431102833, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "20d3b134-eb32-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test", + taskId: "20e9f857-eb32-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["2"], + selectedCase: "2", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + "": "", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_third", + taskReferenceName: "http_third_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_cool", + taskReferenceName: "http_ref_cool", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 3: [ + { + name: "http_five", + taskReferenceName: "http_ref_five", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_second", + taskReferenceName: "http_second_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_four", + taskReferenceName: "http_ref_four_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: + "(function () {\n const number = Math.floor(Math.random() * (4 - 1 + 1)) + 1;\n return number;\n }())", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + loopOverTask: true, + taskDefinition: null, + queueWaitTime: 0, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_third_ref__1", + retryCount: 0, + seq: 4, + pollCount: 1, + taskDefName: "http_third", + scheduledTime: 1711431102829, + startTime: 1711431102860, + endTime: 1711431102871, + updateTime: 1711431102860, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "20d3b134-eb32-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test", + taskId: "20ebf428-eb32-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Tue, 26 Mar 2024 05:31:42 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 745, + hostName: "orkes-api-sampler-67dfc8cf58-dcsmz", + randomString: "vblryduefgemnwrifggg", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_third", + taskReferenceName: "http_third_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + loopOverTask: true, + taskDefinition: null, + queueWaitTime: 31, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_cool__1", + retryCount: 0, + seq: 5, + pollCount: 1, + taskDefName: "http_cool", + scheduledTime: 1711431102873, + startTime: 1711431102970, + endTime: 1711431102981, + updateTime: 1711431102971, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "20d3b134-eb32-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test", + taskId: "20f2aae9-eb32-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 05:31:42 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4701, + hostName: "orkes-api-sampler-67dfc8cf58-ktrql", + randomString: "zflljulcusapgwfaiytx", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_cool", + taskReferenceName: "http_ref_cool", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + loopOverTask: true, + taskDefinition: null, + queueWaitTime: 97, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + "": "", + hasChildren: "true", + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref__2", + retryCount: 0, + seq: 6, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1711431103002, + startTime: 1711431103002, + endTime: 1711431103002, + updateTime: 1711431103002, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "20d3b134-eb32-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test", + taskId: "2104100a-eb32-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + "": "", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_third", + taskReferenceName: "http_third_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_cool", + taskReferenceName: "http_ref_cool", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 3: [ + { + name: "http_five", + taskReferenceName: "http_ref_five", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_second", + taskReferenceName: "http_second_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_four", + taskReferenceName: "http_ref_four_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: + "(function () {\n const number = Math.floor(Math.random() * (4 - 1 + 1)) + 1;\n return number;\n }())", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 2, + subworkflowChanged: false, + loopOverTask: true, + taskDefinition: null, + queueWaitTime: 0, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_second_ref__2", + retryCount: 0, + seq: 7, + pollCount: 1, + taskDefName: "http_second", + scheduledTime: 1711431103000, + startTime: 1711431103081, + endTime: 1711431103090, + updateTime: 1711431103081, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "20d3b134-eb32-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test", + taskId: "21060bdb-eb32-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 05:31:43 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 6714, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "gspbuyqidhyripmblnhh", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_second", + taskReferenceName: "http_second_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 2, + subworkflowChanged: false, + loopOverTask: true, + taskDefinition: null, + queueWaitTime: 81, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_four_ref__2", + retryCount: 0, + seq: 8, + pollCount: 1, + taskDefName: "http_four", + scheduledTime: 1711431103094, + startTime: 1711431103191, + endTime: 1711431103201, + updateTime: 1711431103191, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "20d3b134-eb32-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test", + taskId: "21143cac-eb32-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 05:31:43 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 3391, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "qoauwnmkptdtwonrallz", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_four", + taskReferenceName: "http_ref_four_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 2, + subworkflowChanged: false, + loopOverTask: true, + taskDefinition: null, + queueWaitTime: 97, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + "": "", + hasChildren: "true", + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref__3", + retryCount: 0, + seq: 9, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1711431103218, + startTime: 1711431103219, + endTime: 1711431103219, + updateTime: 1711431103219, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "20d3b134-eb32-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test", + taskId: "21257abd-eb32-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + "": "", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_third", + taskReferenceName: "http_third_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_cool", + taskReferenceName: "http_ref_cool", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 3: [ + { + name: "http_five", + taskReferenceName: "http_ref_five", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_second", + taskReferenceName: "http_second_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_four", + taskReferenceName: "http_ref_four_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: + "(function () {\n const number = Math.floor(Math.random() * (4 - 1 + 1)) + 1;\n return number;\n }())", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 3, + subworkflowChanged: false, + loopOverTask: true, + taskDefinition: null, + queueWaitTime: 1, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_second_ref__3", + retryCount: 0, + seq: 10, + pollCount: 1, + taskDefName: "http_second", + scheduledTime: 1711431103217, + startTime: 1711431103301, + endTime: 1711431103311, + updateTime: 1711431103301, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "20d3b134-eb32-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test", + taskId: "2127286e-eb32-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Tue, 26 Mar 2024 05:31:43 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 591, + hostName: "orkes-api-sampler-67dfc8cf58-mzb8h", + randomString: "osikimmicsohimctzynf", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_second", + taskReferenceName: "http_second_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 3, + subworkflowChanged: false, + loopOverTask: true, + taskDefinition: null, + queueWaitTime: 84, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_four_ref__3", + retryCount: 0, + seq: 11, + pollCount: 1, + taskDefName: "http_four", + scheduledTime: 1711431103313, + startTime: 1711431103410, + endTime: 1711431103420, + updateTime: 1711431103411, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "20d3b134-eb32-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test", + taskId: "2135ce6f-eb32-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 05:31:43 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9953, + hostName: "orkes-api-sampler-67dfc8cf58-7l8kb", + randomString: "mfdhzhvuammftfncjduz", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_four", + taskReferenceName: "http_ref_four_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 3, + subworkflowChanged: false, + loopOverTask: true, + taskDefinition: null, + queueWaitTime: 97, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + "": "", + hasChildren: "true", + _createdBy: "najeeb.thangal@orkes.io", + case: "4", + }, + referenceTaskName: "switch_ref__4", + retryCount: 0, + seq: 12, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1711431103439, + startTime: 1711431103440, + endTime: 1711431103440, + updateTime: 1711431103440, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "20d3b134-eb32-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test", + taskId: "21470c80-eb32-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["4"], + selectedCase: "4", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + "": "", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_third", + taskReferenceName: "http_third_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_cool", + taskReferenceName: "http_ref_cool", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 3: [ + { + name: "http_five", + taskReferenceName: "http_ref_five", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_second", + taskReferenceName: "http_second_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_four", + taskReferenceName: "http_ref_four_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: + "(function () {\n const number = Math.floor(Math.random() * (4 - 1 + 1)) + 1;\n return number;\n }())", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 4, + subworkflowChanged: false, + loopOverTask: true, + taskDefinition: null, + queueWaitTime: 1, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_second_ref__4", + retryCount: 0, + seq: 13, + pollCount: 1, + taskDefName: "http_second", + scheduledTime: 1711431103438, + startTime: 1711431103520, + endTime: 1711431103530, + updateTime: 1711431103520, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "20d3b134-eb32-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test", + taskId: "2148e141-eb32-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 05:31:43 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7297, + hostName: "orkes-api-sampler-67dfc8cf58-jk6kd", + randomString: "emnrcqiwfbrkybhsyptc", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_second", + taskReferenceName: "http_second_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 4, + subworkflowChanged: false, + loopOverTask: true, + taskDefinition: null, + queueWaitTime: 82, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_four_ref__4", + retryCount: 0, + seq: 14, + pollCount: 1, + taskDefName: "http_four", + scheduledTime: 1711431103532, + startTime: 1711431103629, + endTime: 1711431103639, + updateTime: 1711431103630, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "20d3b134-eb32-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test", + taskId: "21573922-eb32-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 05:31:43 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 6651, + hostName: "orkes-api-sampler-67dfc8cf58-dcsmz", + randomString: "uctrykkkcchadijylujt", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_four", + taskReferenceName: "http_ref_four_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 4, + subworkflowChanged: false, + loopOverTask: true, + taskDefinition: null, + queueWaitTime: 97, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + "": "", + hasChildren: "true", + _createdBy: "najeeb.thangal@orkes.io", + case: "3", + }, + referenceTaskName: "switch_ref__5", + retryCount: 0, + seq: 15, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1711431103658, + startTime: 1711431103659, + endTime: 1711431103659, + updateTime: 1711431103659, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "20d3b134-eb32-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test", + taskId: "21687733-eb32-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["3"], + selectedCase: "3", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + "": "", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_third", + taskReferenceName: "http_third_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_cool", + taskReferenceName: "http_ref_cool", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 3: [ + { + name: "http_five", + taskReferenceName: "http_ref_five", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_second", + taskReferenceName: "http_second_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_four", + taskReferenceName: "http_ref_four_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: + "(function () {\n const number = Math.floor(Math.random() * (4 - 1 + 1)) + 1;\n return number;\n }())", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 5, + subworkflowChanged: false, + loopOverTask: true, + taskDefinition: null, + queueWaitTime: 1, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_five__5", + retryCount: 0, + seq: 16, + pollCount: 1, + taskDefName: "http_five", + scheduledTime: 1711431103656, + startTime: 1711431103739, + endTime: 1711431103749, + updateTime: 1711431103739, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "20d3b134-eb32-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test", + taskId: "216a24e4-eb32-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 05:31:43 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 1775, + hostName: "orkes-api-sampler-67dfc8cf58-xsh5s", + randomString: "uscnqmblxmnegjlnbtaa", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_five", + taskReferenceName: "http_ref_five", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 5, + subworkflowChanged: false, + loopOverTask: true, + taskDefinition: null, + queueWaitTime: 83, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + "": "", + hasChildren: "true", + _createdBy: "najeeb.thangal@orkes.io", + case: "3", + }, + referenceTaskName: "switch_ref__6", + retryCount: 0, + seq: 17, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1711431103774, + startTime: 1711431103774, + endTime: 1711431103774, + updateTime: 1711431103774, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "20d3b134-eb32-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test", + taskId: "2179b545-eb32-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["3"], + selectedCase: "3", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + "": "", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_third", + taskReferenceName: "http_third_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_cool", + taskReferenceName: "http_ref_cool", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 3: [ + { + name: "http_five", + taskReferenceName: "http_ref_five", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_second", + taskReferenceName: "http_second_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_four", + taskReferenceName: "http_ref_four_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: + "(function () {\n const number = Math.floor(Math.random() * (4 - 1 + 1)) + 1;\n return number;\n }())", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 6, + subworkflowChanged: false, + loopOverTask: true, + taskDefinition: null, + queueWaitTime: 0, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_five__6", + retryCount: 0, + seq: 18, + pollCount: 1, + taskDefName: "http_five", + scheduledTime: 1711431103772, + startTime: 1711431103849, + endTime: 1711431103867, + updateTime: 1711431103849, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "20d3b134-eb32-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test", + taskId: "217bd826-eb32-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 05:31:43 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9639, + hostName: "orkes-api-sampler-67dfc8cf58-ktrql", + randomString: "zwbfptybsvekbfvzbzml", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_five", + taskReferenceName: "http_ref_five", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 6, + subworkflowChanged: false, + loopOverTask: true, + taskDefinition: null, + queueWaitTime: 77, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + "": "", + hasChildren: "true", + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref__7", + retryCount: 0, + seq: 19, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1711431103888, + startTime: 1711431103888, + endTime: 1711431103888, + updateTime: 1711431103888, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "20d3b134-eb32-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test", + taskId: "218b4177-eb32-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + "": "", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_third", + taskReferenceName: "http_third_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_cool", + taskReferenceName: "http_ref_cool", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 3: [ + { + name: "http_five", + taskReferenceName: "http_ref_five", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_second", + taskReferenceName: "http_second_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_four", + taskReferenceName: "http_ref_four_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: + "(function () {\n const number = Math.floor(Math.random() * (4 - 1 + 1)) + 1;\n return number;\n }())", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 7, + subworkflowChanged: false, + loopOverTask: true, + taskDefinition: null, + queueWaitTime: 0, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_second_ref__7", + retryCount: 0, + seq: 20, + pollCount: 1, + taskDefName: "http_second", + scheduledTime: 1711431103885, + startTime: 1711431103967, + endTime: 1711431103976, + updateTime: 1711431103967, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "20d3b134-eb32-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test", + taskId: "218d1638-eb32-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 05:31:43 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2929, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "ksityuwtdupeziewwqyh", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_second", + taskReferenceName: "http_second_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 7, + subworkflowChanged: false, + loopOverTask: true, + taskDefinition: null, + queueWaitTime: 82, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_four_ref__7", + retryCount: 0, + seq: 21, + pollCount: 1, + taskDefName: "http_four", + scheduledTime: 1711431103979, + startTime: 1711431104077, + endTime: 1711431104088, + updateTime: 1711431104078, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "20d3b134-eb32-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test", + taskId: "219b6e19-eb32-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 05:31:44 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 1547, + hostName: "orkes-api-sampler-67dfc8cf58-mzb8h", + randomString: "xlttyspawtakwfzgslan", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_four", + taskReferenceName: "http_ref_four_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 7, + subworkflowChanged: false, + loopOverTask: true, + taskDefinition: null, + queueWaitTime: 98, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + "": "", + hasChildren: "true", + _createdBy: "najeeb.thangal@orkes.io", + case: "2", + }, + referenceTaskName: "switch_ref__8", + retryCount: 0, + seq: 22, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1711431104107, + startTime: 1711431104107, + endTime: 1711431104107, + updateTime: 1711431104107, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "20d3b134-eb32-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test", + taskId: "21ad215a-eb32-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["2"], + selectedCase: "2", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + "": "", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_third", + taskReferenceName: "http_third_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_cool", + taskReferenceName: "http_ref_cool", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 3: [ + { + name: "http_five", + taskReferenceName: "http_ref_five", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_second", + taskReferenceName: "http_second_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_four", + taskReferenceName: "http_ref_four_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: + "(function () {\n const number = Math.floor(Math.random() * (4 - 1 + 1)) + 1;\n return number;\n }())", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 8, + subworkflowChanged: false, + loopOverTask: true, + taskDefinition: null, + queueWaitTime: 0, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_third_ref__8", + retryCount: 0, + seq: 23, + pollCount: 1, + taskDefName: "http_third", + scheduledTime: 1711431104104, + startTime: 1711431104188, + endTime: 1711431104198, + updateTime: 1711431104188, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "20d3b134-eb32-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test", + taskId: "21ae80eb-eb32-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 05:31:44 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5290, + hostName: "orkes-api-sampler-67dfc8cf58-sts78", + randomString: "ucbkmlxiyqnyakiqggrj", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_third", + taskReferenceName: "http_third_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 8, + subworkflowChanged: false, + loopOverTask: true, + taskDefinition: null, + queueWaitTime: 84, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_cool__8", + retryCount: 0, + seq: 24, + pollCount: 1, + taskDefName: "http_cool", + scheduledTime: 1711431104200, + startTime: 1711431104298, + endTime: 1711431104307, + updateTime: 1711431104298, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "20d3b134-eb32-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test", + taskId: "21bd26ec-eb32-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Tue, 26 Mar 2024 05:31:44 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 363, + hostName: "orkes-api-sampler-67dfc8cf58-jk6kd", + randomString: "hhlhczvqswurlwzjclyi", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_cool", + taskReferenceName: "http_ref_cool", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 8, + subworkflowChanged: false, + loopOverTask: true, + taskDefinition: null, + queueWaitTime: 98, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + "": "", + hasChildren: "true", + _createdBy: "najeeb.thangal@orkes.io", + case: "4", + }, + referenceTaskName: "switch_ref__9", + retryCount: 0, + seq: 25, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1711431104328, + startTime: 1711431104328, + endTime: 1711431104328, + updateTime: 1711431104328, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "20d3b134-eb32-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test", + taskId: "21ce64fd-eb32-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["4"], + selectedCase: "4", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + "": "", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_third", + taskReferenceName: "http_third_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_cool", + taskReferenceName: "http_ref_cool", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 3: [ + { + name: "http_five", + taskReferenceName: "http_ref_five", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_second", + taskReferenceName: "http_second_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_four", + taskReferenceName: "http_ref_four_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: + "(function () {\n const number = Math.floor(Math.random() * (4 - 1 + 1)) + 1;\n return number;\n }())", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 9, + subworkflowChanged: false, + loopOverTask: true, + taskDefinition: null, + queueWaitTime: 0, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_second_ref__9", + retryCount: 0, + seq: 26, + pollCount: 1, + taskDefName: "http_second", + scheduledTime: 1711431104326, + startTime: 1711431104408, + endTime: 1711431104419, + updateTime: 1711431104408, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "20d3b134-eb32-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test", + taskId: "21d060ce-eb32-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 05:31:44 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5292, + hostName: "orkes-api-sampler-67dfc8cf58-dcsmz", + randomString: "zejbepozxwbqfguyhkal", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_second", + taskReferenceName: "http_second_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 9, + subworkflowChanged: false, + loopOverTask: true, + taskDefinition: null, + queueWaitTime: 82, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_four_ref__9", + retryCount: 0, + seq: 27, + pollCount: 1, + taskDefName: "http_four", + scheduledTime: 1711431104421, + startTime: 1711431104520, + endTime: 1711431104529, + updateTime: 1711431104520, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "20d3b134-eb32-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test", + taskId: "21dedfbf-eb32-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Tue, 26 Mar 2024 05:31:44 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 234, + hostName: "orkes-api-sampler-67dfc8cf58-xsh5s", + randomString: "ynvpazmgmmwgqjxnilxo", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_four", + taskReferenceName: "http_ref_four_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 9, + subworkflowChanged: false, + loopOverTask: true, + taskDefinition: null, + queueWaitTime: 99, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + "": "", + hasChildren: "true", + _createdBy: "najeeb.thangal@orkes.io", + case: "4", + }, + referenceTaskName: "switch_ref__10", + retryCount: 0, + seq: 28, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1711431104548, + startTime: 1711431104548, + endTime: 1711431104548, + updateTime: 1711431104548, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "20d3b134-eb32-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test", + taskId: "21f044e0-eb32-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["4"], + selectedCase: "4", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + "": "", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_third", + taskReferenceName: "http_third_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_cool", + taskReferenceName: "http_ref_cool", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 3: [ + { + name: "http_five", + taskReferenceName: "http_ref_five", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_second", + taskReferenceName: "http_second_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_four", + taskReferenceName: "http_ref_four_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: + "(function () {\n const number = Math.floor(Math.random() * (4 - 1 + 1)) + 1;\n return number;\n }())", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + loopOverTask: true, + taskDefinition: null, + queueWaitTime: 0, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_second_ref__10", + retryCount: 0, + seq: 29, + pollCount: 1, + taskDefName: "http_second", + scheduledTime: 1711431104546, + startTime: 1711431104629, + endTime: 1711431104639, + updateTime: 1711431104629, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "20d3b134-eb32-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test", + taskId: "21f1f291-eb32-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 05:31:44 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7926, + hostName: "orkes-api-sampler-67dfc8cf58-ktrql", + randomString: "rzcxmijtfvpcvxqrtblg", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_second", + taskReferenceName: "http_second_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + loopOverTask: true, + taskDefinition: null, + queueWaitTime: 83, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_four_ref__10", + retryCount: 0, + seq: 30, + pollCount: 1, + taskDefName: "http_four", + scheduledTime: 1711431104642, + startTime: 1711431104739, + endTime: 1711431104747, + updateTime: 1711431104739, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "20d3b134-eb32-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test", + taskId: "22007182-eb32-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 05:31:44 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5867, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "lztsmmtdznjtgbjialsw", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_four", + taskReferenceName: "http_ref_four_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + loopOverTask: true, + taskDefinition: null, + queueWaitTime: 97, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_last_ref", + retryCount: 0, + seq: 31, + pollCount: 1, + taskDefName: "http_last", + scheduledTime: 1711431104755, + startTime: 1711431104848, + endTime: 1711431104858, + updateTime: 1711431104848, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "20d3b134-eb32-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test", + taskId: "2211d6a3-eb32-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 05:31:44 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 3688, + hostName: "orkes-api-sampler-67dfc8cf58-7l8kb", + randomString: "jiyldckcvjnosuybyjgr", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_last", + taskReferenceName: "http_last_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + loopOverTask: false, + taskDefinition: null, + queueWaitTime: 93, + }, + ], + input: {}, + output: { + response: { + headers: { + "Strict-Transport-Security": ["max-age=15724800; includeSubDomains"], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 05:31:44 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 3688, + hostName: "orkes-api-sampler-67dfc8cf58-7l8kb", + randomString: "jiyldckcvjnosuybyjgr", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + taskToDomain: {}, + failedReferenceTaskNames: [], + workflowDefinition: { + name: "doWhileExample-test", + description: "DoWhile", + version: 1, + tasks: [ + { + name: "http_first", + taskReferenceName: "http_ref_first", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "do_while", + taskReferenceName: "do_while_ref", + inputParameters: { + number: 10, + }, + type: "DO_WHILE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopCondition: + "(function () {\nif ($.do_while_ref['iteration'] < $.number) {\nreturn true;\n}\nreturn false;\n})();", + loopOver: [ + { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + "": "", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_third", + taskReferenceName: "http_third_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_cool", + taskReferenceName: "http_ref_cool", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 3: [ + { + name: "http_five", + taskReferenceName: "http_ref_five", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_second", + taskReferenceName: "http_second_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_four", + taskReferenceName: "http_ref_four_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: + "(function () {\n const number = Math.floor(Math.random() * (4 - 1 + 1)) + 1;\n return number;\n }())", + onStateChange: {}, + }, + ], + evaluatorType: "graaljs", + onStateChange: {}, + }, + { + name: "http_last", + taskReferenceName: "http_last_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + inputParameters: [], + outputParameters: {}, + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: false, + ownerEmail: "najeeb.thangal@orkes.io", + timeoutPolicy: "ALERT_ONLY", + timeoutSeconds: 0, + variables: {}, + inputTemplate: {}, + rateLimitConfig: { + rateLimitKey: "", + concurrentExecLimit: 0, + }, + }, + priority: 0, + variables: {}, + lastRetriedTime: 0, + history: [], + idempotencyKey: "", + rateLimited: false, + startTime: 1711431102670, + workflowName: "doWhileExample-test", + workflowVersion: 1, +}; + +export const sampleExecutionMultiDoWhile = { + ownerApp: "nhandt+006@orkes.io", + createTime: 1711471925001, + updateTime: 1711471927651, + createdBy: "najeeb.thangal@orkes.io", + updatedBy: "", + status: "COMPLETED", + endTime: 1711471927649, + workflowId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + parentWorkflowId: "", + parentWorkflowTaskId: "", + tasks: [ + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_first", + retryCount: 0, + seq: 1, + pollCount: 1, + taskDefName: "http_first", + scheduledTime: 1711471925008, + startTime: 1711471925082, + endTime: 1711471925168, + updateTime: 1711471925082, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2cd6274a-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Tue, 26 Mar 2024 16:52:05 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 141, + hostName: "orkes-api-sampler-67dfc8cf58-sts78", + randomString: "ieflflzwxuioasyrpcuy", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_first", + taskReferenceName: "http_ref_first", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + queueWaitTime: 74, + loopOverTask: false, + taskDefinition: null, + }, + { + taskType: "DO_WHILE", + status: "COMPLETED", + inputData: { + number: 10, + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "do_while_ref", + retryCount: 0, + seq: 2, + pollCount: 0, + taskDefName: "do_while", + scheduledTime: 1711471925176, + startTime: 1711471925176, + endTime: 1711471926648, + updateTime: 1711471926565, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2cef7bab-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + outputData: { + 1: { + http_ref_five: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:05 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8714, + hostName: "orkes-api-sampler-67dfc8cf58-7l8kb", + randomString: "pfoaimvrlugiyrhfimay", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + switch_ref: { + evaluationResult: ["3"], + selectedCase: "3", + }, + }, + 2: { + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_second_ref: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:05 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 1544, + hostName: "orkes-api-sampler-67dfc8cf58-jk6kd", + randomString: "rgiezqolnhzbydafasan", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_four_ref: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Tue, 26 Mar 2024 16:52:05 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 629, + hostName: "orkes-api-sampler-67dfc8cf58-dcsmz", + randomString: "lzlatizbpoqjhzdosgso", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 3: { + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_second_ref: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:05 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4184, + hostName: "orkes-api-sampler-67dfc8cf58-xsh5s", + randomString: "krbshxkajdnwwsfktacy", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_four_ref: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:05 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 6209, + hostName: "orkes-api-sampler-67dfc8cf58-ktrql", + randomString: "yfwguelsuywesbdqtgot", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 4: { + http_ref_five: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:05 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4205, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "jfkmltyjvljzekrrokgb", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + switch_ref: { + evaluationResult: ["3"], + selectedCase: "3", + }, + }, + 5: { + http_ref_five: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:05 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8381, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "qwfsboamyfvitgofhmyx", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + switch_ref: { + evaluationResult: ["3"], + selectedCase: "3", + }, + }, + 6: { + http_ref_five: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:05 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9193, + hostName: "orkes-api-sampler-67dfc8cf58-mzb8h", + randomString: "hodpvolnbhurcviznsae", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + switch_ref: { + evaluationResult: ["3"], + selectedCase: "3", + }, + }, + 7: { + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_second_ref: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:06 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7460, + hostName: "orkes-api-sampler-67dfc8cf58-sts78", + randomString: "gaylbzkwhkcmvnrbqofh", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_four_ref: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:06 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4893, + hostName: "orkes-api-sampler-67dfc8cf58-7l8kb", + randomString: "zschnkxrmpkpqzxqulkt", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 8: { + http_ref_five: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:06 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2499, + hostName: "orkes-api-sampler-67dfc8cf58-jk6kd", + randomString: "yeckxdenrknaxlssksws", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + switch_ref: { + evaluationResult: ["3"], + selectedCase: "3", + }, + }, + 9: { + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_second_ref: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:06 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8649, + hostName: "orkes-api-sampler-67dfc8cf58-dcsmz", + randomString: "ivwrjhwhhptaklledpom", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_four_ref: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:06 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 1574, + hostName: "orkes-api-sampler-67dfc8cf58-xsh5s", + randomString: "hgrpontvfcxtpkaiecau", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 10: { + http_ref_five: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:06 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4135, + hostName: "orkes-api-sampler-67dfc8cf58-ktrql", + randomString: "wcbjbqpgrjidquahiihm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + switch_ref: { + evaluationResult: ["3"], + selectedCase: "3", + }, + }, + iteration: 10, + }, + workflowTask: { + name: "do_while", + taskReferenceName: "do_while_ref", + inputParameters: { + number: 10, + }, + type: "DO_WHILE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopCondition: + "(function () {\nif ($.do_while_ref['iteration'] < $.number) {\nreturn true;\n}\nreturn false;\n})();", + loopOver: [ + { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + "": "", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_third", + taskReferenceName: "http_third_ref", + inputParameters: { + method: "GET", + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_cool", + taskReferenceName: "http_ref_cool", + inputParameters: { + method: "GET", + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 3: [ + { + name: "http_five", + taskReferenceName: "http_ref_five", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_second", + taskReferenceName: "http_second_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_four", + taskReferenceName: "http_ref_four_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: + "(function () {\n const number = Math.floor(Math.random() * (4 - 1 + 1)) + 1;\n return number;\n }())", + onStateChange: {}, + }, + ], + evaluatorType: "graaljs", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + "": "", + hasChildren: "true", + _createdBy: "najeeb.thangal@orkes.io", + case: "3", + }, + referenceTaskName: "switch_ref__1", + retryCount: 0, + seq: 3, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1711471925191, + startTime: 1711471925191, + endTime: 1711471925191, + updateTime: 1711471925191, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2ceff0dc-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["3"], + selectedCase: "3", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + "": "", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_third", + taskReferenceName: "http_third_ref", + inputParameters: { + method: "GET", + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_cool", + taskReferenceName: "http_ref_cool", + inputParameters: { + method: "GET", + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 3: [ + { + name: "http_five", + taskReferenceName: "http_ref_five", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_second", + taskReferenceName: "http_second_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_four", + taskReferenceName: "http_ref_four_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: + "(function () {\n const number = Math.floor(Math.random() * (4 - 1 + 1)) + 1;\n return number;\n }())", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_five__1", + retryCount: 0, + seq: 4, + pollCount: 1, + taskDefName: "http_five", + scheduledTime: 1711471925184, + startTime: 1711471925196, + endTime: 1711471925207, + updateTime: 1711471925196, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2cf1024d-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:05 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8714, + hostName: "orkes-api-sampler-67dfc8cf58-7l8kb", + randomString: "pfoaimvrlugiyrhfimay", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_five", + taskReferenceName: "http_ref_five", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 12, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + "": "", + hasChildren: "true", + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref__2", + retryCount: 0, + seq: 5, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1711471925231, + startTime: 1711471925231, + endTime: 1711471925231, + updateTime: 1711471925231, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2cf60b5e-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + "": "", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_third", + taskReferenceName: "http_third_ref", + inputParameters: { + method: "GET", + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_cool", + taskReferenceName: "http_ref_cool", + inputParameters: { + method: "GET", + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 3: [ + { + name: "http_five", + taskReferenceName: "http_ref_five", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_second", + taskReferenceName: "http_second_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_four", + taskReferenceName: "http_ref_four_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: + "(function () {\n const number = Math.floor(Math.random() * (4 - 1 + 1)) + 1;\n return number;\n }())", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 2, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_second_ref__2", + retryCount: 0, + seq: 6, + pollCount: 1, + taskDefName: "http_second", + scheduledTime: 1711471925227, + startTime: 1711471925307, + endTime: 1711471925317, + updateTime: 1711471925307, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2cf791ff-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:05 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 1544, + hostName: "orkes-api-sampler-67dfc8cf58-jk6kd", + randomString: "rgiezqolnhzbydafasan", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_second", + taskReferenceName: "http_second_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 2, + subworkflowChanged: false, + queueWaitTime: 80, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_four_ref__2", + retryCount: 0, + seq: 7, + pollCount: 1, + taskDefName: "http_four", + scheduledTime: 1711471925322, + startTime: 1711471925417, + endTime: 1711471925427, + updateTime: 1711471925417, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2d0610f0-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Tue, 26 Mar 2024 16:52:05 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 629, + hostName: "orkes-api-sampler-67dfc8cf58-dcsmz", + randomString: "lzlatizbpoqjhzdosgso", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_four", + taskReferenceName: "http_ref_four_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 2, + subworkflowChanged: false, + queueWaitTime: 95, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + "": "", + hasChildren: "true", + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref__3", + retryCount: 0, + seq: 8, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1711471925449, + startTime: 1711471925449, + endTime: 1711471925449, + updateTime: 1711471925449, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2d17c431-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + "": "", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_third", + taskReferenceName: "http_third_ref", + inputParameters: { + method: "GET", + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_cool", + taskReferenceName: "http_ref_cool", + inputParameters: { + method: "GET", + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 3: [ + { + name: "http_five", + taskReferenceName: "http_ref_five", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_second", + taskReferenceName: "http_second_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_four", + taskReferenceName: "http_ref_four_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: + "(function () {\n const number = Math.floor(Math.random() * (4 - 1 + 1)) + 1;\n return number;\n }())", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 3, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_second_ref__3", + retryCount: 0, + seq: 9, + pollCount: 1, + taskDefName: "http_second", + scheduledTime: 1711471925445, + startTime: 1711471925527, + endTime: 1711471925538, + updateTime: 1711471925527, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2d18d5a2-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:05 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4184, + hostName: "orkes-api-sampler-67dfc8cf58-xsh5s", + randomString: "krbshxkajdnwwsfktacy", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_second", + taskReferenceName: "http_second_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 3, + subworkflowChanged: false, + queueWaitTime: 82, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_four_ref__3", + retryCount: 0, + seq: 10, + pollCount: 1, + taskDefName: "http_four", + scheduledTime: 1711471925543, + startTime: 1711471925638, + endTime: 1711471925648, + updateTime: 1711471925638, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2d27c9c3-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:05 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 6209, + hostName: "orkes-api-sampler-67dfc8cf58-ktrql", + randomString: "yfwguelsuywesbdqtgot", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_four", + taskReferenceName: "http_ref_four_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 3, + subworkflowChanged: false, + queueWaitTime: 95, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + "": "", + hasChildren: "true", + _createdBy: "najeeb.thangal@orkes.io", + case: "3", + }, + referenceTaskName: "switch_ref__4", + retryCount: 0, + seq: 11, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1711471925677, + startTime: 1711471925677, + endTime: 1711471925677, + updateTime: 1711471925677, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2d39a414-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["3"], + selectedCase: "3", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + "": "", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_third", + taskReferenceName: "http_third_ref", + inputParameters: { + method: "GET", + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_cool", + taskReferenceName: "http_ref_cool", + inputParameters: { + method: "GET", + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 3: [ + { + name: "http_five", + taskReferenceName: "http_ref_five", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_second", + taskReferenceName: "http_second_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_four", + taskReferenceName: "http_ref_four_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: + "(function () {\n const number = Math.floor(Math.random() * (4 - 1 + 1)) + 1;\n return number;\n }())", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 4, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_five__4", + retryCount: 0, + seq: 12, + pollCount: 1, + taskDefName: "http_five", + scheduledTime: 1711471925667, + startTime: 1711471925748, + endTime: 1711471925760, + updateTime: 1711471925748, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2d3ab585-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:05 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4205, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "jfkmltyjvljzekrrokgb", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_five", + taskReferenceName: "http_ref_five", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 4, + subworkflowChanged: false, + queueWaitTime: 81, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + "": "", + hasChildren: "true", + _createdBy: "najeeb.thangal@orkes.io", + case: "3", + }, + referenceTaskName: "switch_ref__5", + retryCount: 0, + seq: 13, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1711471925782, + startTime: 1711471925782, + endTime: 1711471925782, + updateTime: 1711471925782, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2d4a6cf6-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["3"], + selectedCase: "3", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + "": "", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_third", + taskReferenceName: "http_third_ref", + inputParameters: { + method: "GET", + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_cool", + taskReferenceName: "http_ref_cool", + inputParameters: { + method: "GET", + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 3: [ + { + name: "http_five", + taskReferenceName: "http_ref_five", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_second", + taskReferenceName: "http_second_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_four", + taskReferenceName: "http_ref_four_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: + "(function () {\n const number = Math.floor(Math.random() * (4 - 1 + 1)) + 1;\n return number;\n }())", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 5, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_five__5", + retryCount: 0, + seq: 14, + pollCount: 1, + taskDefName: "http_five", + scheduledTime: 1711471925777, + startTime: 1711471925859, + endTime: 1711471925876, + updateTime: 1711471925859, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2d4b7e67-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:05 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8381, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "qwfsboamyfvitgofhmyx", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_five", + taskReferenceName: "http_ref_five", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 5, + subworkflowChanged: false, + queueWaitTime: 82, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + "": "", + hasChildren: "true", + _createdBy: "najeeb.thangal@orkes.io", + case: "3", + }, + referenceTaskName: "switch_ref__6", + retryCount: 0, + seq: 15, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1711471925899, + startTime: 1711471925899, + endTime: 1711471925899, + updateTime: 1711471925899, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2d5bf928-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["3"], + selectedCase: "3", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + "": "", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_third", + taskReferenceName: "http_third_ref", + inputParameters: { + method: "GET", + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_cool", + taskReferenceName: "http_ref_cool", + inputParameters: { + method: "GET", + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 3: [ + { + name: "http_five", + taskReferenceName: "http_ref_five", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_second", + taskReferenceName: "http_second_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_four", + taskReferenceName: "http_ref_four_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: + "(function () {\n const number = Math.floor(Math.random() * (4 - 1 + 1)) + 1;\n return number;\n }())", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 6, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_five__6", + retryCount: 0, + seq: 16, + pollCount: 1, + taskDefName: "http_five", + scheduledTime: 1711471925894, + startTime: 1711471925970, + endTime: 1711471925980, + updateTime: 1711471925970, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2d5d58b9-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:05 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9193, + hostName: "orkes-api-sampler-67dfc8cf58-mzb8h", + randomString: "hodpvolnbhurcviznsae", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_five", + taskReferenceName: "http_ref_five", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 6, + subworkflowChanged: false, + queueWaitTime: 76, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + "": "", + hasChildren: "true", + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref__7", + retryCount: 0, + seq: 17, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1711471926000, + startTime: 1711471926000, + endTime: 1711471926000, + updateTime: 1711471926000, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2d6bb09a-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + "": "", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_third", + taskReferenceName: "http_third_ref", + inputParameters: { + method: "GET", + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_cool", + taskReferenceName: "http_ref_cool", + inputParameters: { + method: "GET", + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 3: [ + { + name: "http_five", + taskReferenceName: "http_ref_five", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_second", + taskReferenceName: "http_second_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_four", + taskReferenceName: "http_ref_four_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: + "(function () {\n const number = Math.floor(Math.random() * (4 - 1 + 1)) + 1;\n return number;\n }())", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 7, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_second_ref__7", + retryCount: 0, + seq: 18, + pollCount: 1, + taskDefName: "http_second", + scheduledTime: 1711471925996, + startTime: 1711471926080, + endTime: 1711471926089, + updateTime: 1711471926080, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2d6ce91b-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:06 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7460, + hostName: "orkes-api-sampler-67dfc8cf58-sts78", + randomString: "gaylbzkwhkcmvnrbqofh", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_second", + taskReferenceName: "http_second_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 7, + subworkflowChanged: false, + queueWaitTime: 84, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_four_ref__7", + retryCount: 0, + seq: 19, + pollCount: 1, + taskDefName: "http_four", + scheduledTime: 1711471926095, + startTime: 1711471926190, + endTime: 1711471926199, + updateTime: 1711471926190, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2d7bdd3c-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:06 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4893, + hostName: "orkes-api-sampler-67dfc8cf58-7l8kb", + randomString: "zschnkxrmpkpqzxqulkt", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_four", + taskReferenceName: "http_ref_four_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 7, + subworkflowChanged: false, + queueWaitTime: 95, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + "": "", + hasChildren: "true", + _createdBy: "najeeb.thangal@orkes.io", + case: "3", + }, + referenceTaskName: "switch_ref__8", + retryCount: 0, + seq: 20, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1711471926223, + startTime: 1711471926223, + endTime: 1711471926223, + updateTime: 1711471926223, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2d8d907d-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["3"], + selectedCase: "3", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + "": "", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_third", + taskReferenceName: "http_third_ref", + inputParameters: { + method: "GET", + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_cool", + taskReferenceName: "http_ref_cool", + inputParameters: { + method: "GET", + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 3: [ + { + name: "http_five", + taskReferenceName: "http_ref_five", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_second", + taskReferenceName: "http_second_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_four", + taskReferenceName: "http_ref_four_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: + "(function () {\n const number = Math.floor(Math.random() * (4 - 1 + 1)) + 1;\n return number;\n }())", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 8, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_five__8", + retryCount: 0, + seq: 21, + pollCount: 1, + taskDefName: "http_five", + scheduledTime: 1711471926218, + startTime: 1711471926300, + endTime: 1711471926308, + updateTime: 1711471926300, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2d8ec8fe-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:06 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2499, + hostName: "orkes-api-sampler-67dfc8cf58-jk6kd", + randomString: "yeckxdenrknaxlssksws", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_five", + taskReferenceName: "http_ref_five", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 8, + subworkflowChanged: false, + queueWaitTime: 82, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + "": "", + hasChildren: "true", + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref__9", + retryCount: 0, + seq: 22, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1711471926330, + startTime: 1711471926330, + endTime: 1711471926330, + updateTime: 1711471926330, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2d9e0b3f-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + "": "", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_third", + taskReferenceName: "http_third_ref", + inputParameters: { + method: "GET", + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_cool", + taskReferenceName: "http_ref_cool", + inputParameters: { + method: "GET", + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 3: [ + { + name: "http_five", + taskReferenceName: "http_ref_five", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_second", + taskReferenceName: "http_second_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_four", + taskReferenceName: "http_ref_four_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: + "(function () {\n const number = Math.floor(Math.random() * (4 - 1 + 1)) + 1;\n return number;\n }())", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 9, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_second_ref__9", + retryCount: 0, + seq: 23, + pollCount: 1, + taskDefName: "http_second", + scheduledTime: 1711471926325, + startTime: 1711471926409, + endTime: 1711471926419, + updateTime: 1711471926409, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2d9f1cb0-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:06 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8649, + hostName: "orkes-api-sampler-67dfc8cf58-dcsmz", + randomString: "ivwrjhwhhptaklledpom", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_second", + taskReferenceName: "http_second_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 9, + subworkflowChanged: false, + queueWaitTime: 84, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_four_ref__9", + retryCount: 0, + seq: 24, + pollCount: 1, + taskDefName: "http_four", + scheduledTime: 1711471926428, + startTime: 1711471926519, + endTime: 1711471926541, + updateTime: 1711471926519, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2daed421-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:06 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 1574, + hostName: "orkes-api-sampler-67dfc8cf58-xsh5s", + randomString: "hgrpontvfcxtpkaiecau", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_four", + taskReferenceName: "http_ref_four_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 9, + subworkflowChanged: false, + queueWaitTime: 91, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + "": "", + hasChildren: "true", + _createdBy: "najeeb.thangal@orkes.io", + case: "3", + }, + referenceTaskName: "switch_ref__10", + retryCount: 0, + seq: 25, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1711471926563, + startTime: 1711471926563, + endTime: 1711471926563, + updateTime: 1711471926563, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2dc1bfe2-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["3"], + selectedCase: "3", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + "": "", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_third", + taskReferenceName: "http_third_ref", + inputParameters: { + method: "GET", + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_cool", + taskReferenceName: "http_ref_cool", + inputParameters: { + method: "GET", + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 3: [ + { + name: "http_five", + taskReferenceName: "http_ref_five", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_second", + taskReferenceName: "http_second_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_four", + taskReferenceName: "http_ref_four_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: + "(function () {\n const number = Math.floor(Math.random() * (4 - 1 + 1)) + 1;\n return number;\n }())", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_five__10", + retryCount: 0, + seq: 26, + pollCount: 1, + taskDefName: "http_five", + scheduledTime: 1711471926559, + startTime: 1711471926628, + endTime: 1711471926638, + updateTime: 1711471926628, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2dc2d153-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:06 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4135, + hostName: "orkes-api-sampler-67dfc8cf58-ktrql", + randomString: "wcbjbqpgrjidquahiihm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_five", + taskReferenceName: "http_ref_five", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 69, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_last_ref", + retryCount: 0, + seq: 27, + pollCount: 1, + taskDefName: "http_last", + scheduledTime: 1711471926651, + startTime: 1711471926738, + endTime: 1711471926749, + updateTime: 1711471926738, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2dd0b404-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Tue, 26 Mar 2024 16:52:06 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 316, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "nqfvddyhiefplxkfpvce", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_last", + taskReferenceName: "http_last_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + queueWaitTime: 87, + loopOverTask: false, + taskDefinition: null, + }, + { + taskType: "DO_WHILE", + status: "COMPLETED", + inputData: { + number: "8", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "do_while_ref_1", + retryCount: 0, + seq: 28, + pollCount: 0, + taskDefName: "do_while_1", + scheduledTime: 1711471926757, + startTime: 1711471926757, + endTime: 1711471927646, + updateTime: 1711471927554, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2de09285-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + outputData: { + 1: { + http_new_dowhile_one_ref: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:06 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8059, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "ffjecsjmhafljzlfmomh", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + switch_ref_1: { + evaluationResult: ["4"], + selectedCase: "4", + }, + }, + 2: { + http_new_dowhile_two_ref: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Tue, 26 Mar 2024 16:52:06 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 840, + hostName: "orkes-api-sampler-67dfc8cf58-mzb8h", + randomString: "qdxthpirsqnhyylytlwp", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + switch_ref_1: { + evaluationResult: ["2"], + selectedCase: "2", + }, + }, + 3: { + http_new_dowhile_two_ref: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:07 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5889, + hostName: "orkes-api-sampler-67dfc8cf58-sts78", + randomString: "vqqjoysaxxifvnzsrgdf", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + switch_ref_1: { + evaluationResult: ["2"], + selectedCase: "2", + }, + }, + 4: { + http_new_dowhile_one_ref: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:07 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 3285, + hostName: "orkes-api-sampler-67dfc8cf58-7l8kb", + randomString: "omiwzhgmfgecamejmqnw", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + switch_ref_1: { + evaluationResult: ["1"], + selectedCase: "1", + }, + }, + 5: { + http_new_dowhile_one_ref: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Tue, 26 Mar 2024 16:52:07 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 379, + hostName: "orkes-api-sampler-67dfc8cf58-jk6kd", + randomString: "vqbajywpvhuofzyaqukp", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + switch_ref_1: { + evaluationResult: ["4"], + selectedCase: "4", + }, + }, + 6: { + http_new_dowhile_one_ref: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:07 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2130, + hostName: "orkes-api-sampler-67dfc8cf58-dcsmz", + randomString: "luiyzqmpzmqcyfrwaxht", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + switch_ref_1: { + evaluationResult: ["1"], + selectedCase: "1", + }, + }, + 7: { + http_ref_1: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:07 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9020, + hostName: "orkes-api-sampler-67dfc8cf58-xsh5s", + randomString: "hdnsxoswwdzjhqrzpegq", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + switch_ref_1: { + evaluationResult: ["3"], + selectedCase: "3", + }, + }, + 8: { + http_ref_1: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:07 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 6546, + hostName: "orkes-api-sampler-67dfc8cf58-ktrql", + randomString: "tunauwsfdcbqgktnoayp", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + switch_ref_1: { + evaluationResult: ["3"], + selectedCase: "3", + }, + }, + iteration: 8, + }, + workflowTask: { + name: "do_while_1", + taskReferenceName: "do_while_ref_1", + inputParameters: { + number: "8", + }, + type: "DO_WHILE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopCondition: + "(function () {\nif ($.do_while_ref_1['iteration'] < $.number) {\nreturn true;\n}\nreturn false;\n})();", + loopOver: [ + { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_new_dowhile_two", + taskReferenceName: "http_new_dowhile_two_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 3: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_new_dowhile_one", + taskReferenceName: "http_new_dowhile_one_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: + "(function () {\n const number = Math.floor(Math.random() * (4 - 1 + 1)) + 1;\n return number;\n }())", + onStateChange: {}, + }, + ], + evaluatorType: "graaljs", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + workflowPriority: 0, + iteration: 8, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: "", + _createdBy: "najeeb.thangal@orkes.io", + case: "4", + }, + referenceTaskName: "switch_ref_1__1", + retryCount: 0, + seq: 29, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1711471926772, + startTime: 1711471926772, + endTime: 1711471926772, + updateTime: 1711471926772, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2de12ec6-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["4"], + selectedCase: "4", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_new_dowhile_two", + taskReferenceName: "http_new_dowhile_two_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 3: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_new_dowhile_one", + taskReferenceName: "http_new_dowhile_one_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: + "(function () {\n const number = Math.floor(Math.random() * (4 - 1 + 1)) + 1;\n return number;\n }())", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_new_dowhile_one_ref__1", + retryCount: 0, + seq: 30, + pollCount: 1, + taskDefName: "http_new_dowhile_one", + scheduledTime: 1711471926767, + startTime: 1711471926849, + endTime: 1711471926861, + updateTime: 1711471926849, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2de28e57-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:06 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8059, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "ffjecsjmhafljzlfmomh", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_new_dowhile_one", + taskReferenceName: "http_new_dowhile_one_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 82, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: "", + _createdBy: "najeeb.thangal@orkes.io", + case: "2", + }, + referenceTaskName: "switch_ref_1__2", + retryCount: 0, + seq: 31, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1711471926883, + startTime: 1711471926883, + endTime: 1711471926883, + updateTime: 1711471926883, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2df293e8-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["2"], + selectedCase: "2", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_new_dowhile_two", + taskReferenceName: "http_new_dowhile_two_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 3: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_new_dowhile_one", + taskReferenceName: "http_new_dowhile_one_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: + "(function () {\n const number = Math.floor(Math.random() * (4 - 1 + 1)) + 1;\n return number;\n }())", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 2, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_new_dowhile_two_ref__2", + retryCount: 0, + seq: 32, + pollCount: 1, + taskDefName: "http_new_dowhile_two", + scheduledTime: 1711471926879, + startTime: 1711471926958, + endTime: 1711471926969, + updateTime: 1711471926958, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2df3a559-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Tue, 26 Mar 2024 16:52:06 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 840, + hostName: "orkes-api-sampler-67dfc8cf58-mzb8h", + randomString: "qdxthpirsqnhyylytlwp", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_new_dowhile_two", + taskReferenceName: "http_new_dowhile_two_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 2, + subworkflowChanged: false, + queueWaitTime: 79, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: "", + _createdBy: "najeeb.thangal@orkes.io", + case: "2", + }, + referenceTaskName: "switch_ref_1__3", + retryCount: 0, + seq: 33, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1711471926990, + startTime: 1711471926990, + endTime: 1711471926990, + updateTime: 1711471926990, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2e02e79a-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["2"], + selectedCase: "2", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_new_dowhile_two", + taskReferenceName: "http_new_dowhile_two_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 3: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_new_dowhile_one", + taskReferenceName: "http_new_dowhile_one_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: + "(function () {\n const number = Math.floor(Math.random() * (4 - 1 + 1)) + 1;\n return number;\n }())", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 3, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_new_dowhile_two_ref__3", + retryCount: 0, + seq: 34, + pollCount: 1, + taskDefName: "http_new_dowhile_two", + scheduledTime: 1711471926986, + startTime: 1711471927070, + endTime: 1711471927079, + updateTime: 1711471927070, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2e03f90b-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:07 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5889, + hostName: "orkes-api-sampler-67dfc8cf58-sts78", + randomString: "vqqjoysaxxifvnzsrgdf", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_new_dowhile_two", + taskReferenceName: "http_new_dowhile_two_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 3, + subworkflowChanged: false, + queueWaitTime: 84, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: "", + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref_1__4", + retryCount: 0, + seq: 35, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1711471927102, + startTime: 1711471927102, + endTime: 1711471927102, + updateTime: 1711471927102, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2e13b07c-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_new_dowhile_two", + taskReferenceName: "http_new_dowhile_two_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 3: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_new_dowhile_one", + taskReferenceName: "http_new_dowhile_one_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: + "(function () {\n const number = Math.floor(Math.random() * (4 - 1 + 1)) + 1;\n return number;\n }())", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 4, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_new_dowhile_one_ref__4", + retryCount: 0, + seq: 36, + pollCount: 1, + taskDefName: "http_new_dowhile_one", + scheduledTime: 1711471927097, + startTime: 1711471927179, + endTime: 1711471927189, + updateTime: 1711471927179, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2e14e8fd-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:07 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 3285, + hostName: "orkes-api-sampler-67dfc8cf58-7l8kb", + randomString: "omiwzhgmfgecamejmqnw", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_new_dowhile_one", + taskReferenceName: "http_new_dowhile_one_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 4, + subworkflowChanged: false, + queueWaitTime: 82, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: "", + _createdBy: "najeeb.thangal@orkes.io", + case: "4", + }, + referenceTaskName: "switch_ref_1__5", + retryCount: 0, + seq: 37, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1711471927214, + startTime: 1711471927214, + endTime: 1711471927214, + updateTime: 1711471927214, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2e24a06e-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["4"], + selectedCase: "4", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_new_dowhile_two", + taskReferenceName: "http_new_dowhile_two_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 3: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_new_dowhile_one", + taskReferenceName: "http_new_dowhile_one_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: + "(function () {\n const number = Math.floor(Math.random() * (4 - 1 + 1)) + 1;\n return number;\n }())", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 5, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_new_dowhile_one_ref__5", + retryCount: 0, + seq: 38, + pollCount: 1, + taskDefName: "http_new_dowhile_one", + scheduledTime: 1711471927210, + startTime: 1711471927291, + endTime: 1711471927306, + updateTime: 1711471927291, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2e25ffff-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Tue, 26 Mar 2024 16:52:07 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 379, + hostName: "orkes-api-sampler-67dfc8cf58-jk6kd", + randomString: "vqbajywpvhuofzyaqukp", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_new_dowhile_one", + taskReferenceName: "http_new_dowhile_one_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 5, + subworkflowChanged: false, + queueWaitTime: 81, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: "", + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref_1__6", + retryCount: 0, + seq: 39, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1711471927364, + startTime: 1711471927364, + endTime: 1711471927364, + updateTime: 1711471927364, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2e3653b0-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_new_dowhile_two", + taskReferenceName: "http_new_dowhile_two_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 3: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_new_dowhile_one", + taskReferenceName: "http_new_dowhile_one_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: + "(function () {\n const number = Math.floor(Math.random() * (4 - 1 + 1)) + 1;\n return number;\n }())", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 6, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_new_dowhile_one_ref__6", + retryCount: 0, + seq: 40, + pollCount: 1, + taskDefName: "http_new_dowhile_one", + scheduledTime: 1711471927358, + startTime: 1711471927407, + endTime: 1711471927426, + updateTime: 1711471927407, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2e3cbc51-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:07 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2130, + hostName: "orkes-api-sampler-67dfc8cf58-dcsmz", + randomString: "luiyzqmpzmqcyfrwaxht", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_new_dowhile_one", + taskReferenceName: "http_new_dowhile_one_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 6, + subworkflowChanged: false, + queueWaitTime: 49, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: "", + _createdBy: "najeeb.thangal@orkes.io", + case: "3", + }, + referenceTaskName: "switch_ref_1__7", + retryCount: 0, + seq: 41, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1711471927471, + startTime: 1711471927471, + endTime: 1711471927471, + updateTime: 1711471927471, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2e496682-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["3"], + selectedCase: "3", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_new_dowhile_two", + taskReferenceName: "http_new_dowhile_two_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 3: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_new_dowhile_one", + taskReferenceName: "http_new_dowhile_one_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: + "(function () {\n const number = Math.floor(Math.random() * (4 - 1 + 1)) + 1;\n return number;\n }())", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 7, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_1__7", + retryCount: 0, + seq: 42, + pollCount: 1, + taskDefName: "http_1", + scheduledTime: 1711471927465, + startTime: 1711471927518, + endTime: 1711471927527, + updateTime: 1711471927518, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2e4d1003-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:07 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9020, + hostName: "orkes-api-sampler-67dfc8cf58-xsh5s", + randomString: "hdnsxoswwdzjhqrzpegq", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 7, + subworkflowChanged: false, + queueWaitTime: 53, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: "", + _createdBy: "najeeb.thangal@orkes.io", + case: "3", + }, + referenceTaskName: "switch_ref_1__8", + retryCount: 0, + seq: 43, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1711471927551, + startTime: 1711471927551, + endTime: 1711471927551, + updateTime: 1711471927551, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2e580c84-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["3"], + selectedCase: "3", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_new_dowhile_two", + taskReferenceName: "http_new_dowhile_two_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 3: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_new_dowhile_one", + taskReferenceName: "http_new_dowhile_one_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: + "(function () {\n const number = Math.floor(Math.random() * (4 - 1 + 1)) + 1;\n return number;\n }())", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 8, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_1__8", + retryCount: 0, + seq: 44, + pollCount: 1, + taskDefName: "http_1", + scheduledTime: 1711471927543, + startTime: 1711471927627, + endTime: 1711471927636, + updateTime: 1711471927627, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "2cd515d9-eb91-11ee-8b0a-0e6876359850", + workflowType: "doWhileExample-test-multi-dowhile", + taskId: "2e58f6e5-eb91-11ee-8b0a-0e6876359850", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-7d46b7c7b5-m5hcf", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:07 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 6546, + hostName: "orkes-api-sampler-67dfc8cf58-ktrql", + randomString: "tunauwsfdcbqgktnoayp", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 8, + subworkflowChanged: false, + queueWaitTime: 84, + loopOverTask: true, + taskDefinition: null, + }, + ], + input: {}, + output: { + response: { + headers: { + "Strict-Transport-Security": ["max-age=15724800; includeSubDomains"], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Tue, 26 Mar 2024 16:52:07 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 6546, + hostName: "orkes-api-sampler-67dfc8cf58-ktrql", + randomString: "tunauwsfdcbqgktnoayp", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + taskToDomain: {}, + failedReferenceTaskNames: [], + workflowDefinition: { + name: "doWhileExample-test-multi-dowhile", + description: "DoWhile", + version: 1, + tasks: [ + { + name: "http_first", + taskReferenceName: "http_ref_first", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "do_while", + taskReferenceName: "do_while_ref", + inputParameters: { + number: 10, + }, + type: "DO_WHILE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopCondition: + "(function () {\nif ($.do_while_ref['iteration'] < $.number) {\nreturn true;\n}\nreturn false;\n})();", + loopOver: [ + { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + "": "", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_third", + taskReferenceName: "http_third_ref", + inputParameters: { + method: "GET", + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_cool", + taskReferenceName: "http_ref_cool", + inputParameters: { + method: "GET", + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 3: [ + { + name: "http_five", + taskReferenceName: "http_ref_five", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_second", + taskReferenceName: "http_second_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_four", + taskReferenceName: "http_ref_four_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: + "(function () {\n const number = Math.floor(Math.random() * (4 - 1 + 1)) + 1;\n return number;\n }())", + onStateChange: {}, + }, + ], + evaluatorType: "graaljs", + onStateChange: {}, + }, + { + name: "http_last", + taskReferenceName: "http_last_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "do_while_1", + taskReferenceName: "do_while_ref_1", + inputParameters: { + number: "8", + }, + type: "DO_WHILE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopCondition: + "(function () {\nif ($.do_while_ref_1['iteration'] < $.number) {\nreturn true;\n}\nreturn false;\n})();", + loopOver: [ + { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_new_dowhile_two", + taskReferenceName: "http_new_dowhile_two_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 3: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_new_dowhile_one", + taskReferenceName: "http_new_dowhile_one_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: + "(function () {\n const number = Math.floor(Math.random() * (4 - 1 + 1)) + 1;\n return number;\n }())", + onStateChange: {}, + }, + ], + evaluatorType: "graaljs", + onStateChange: {}, + }, + ], + inputParameters: [], + outputParameters: {}, + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: false, + ownerEmail: "najeeb.thangal@orkes.io", + timeoutPolicy: "ALERT_ONLY", + timeoutSeconds: 0, + variables: {}, + inputTemplate: {}, + rateLimitConfig: { + rateLimitKey: "", + concurrentExecLimit: 0, + }, + }, + priority: 0, + variables: {}, + lastRetriedTime: 0, + history: [], + idempotencyKey: "", + rateLimited: false, + startTime: 1711471925001, + workflowName: "doWhileExample-test-multi-dowhile", + workflowVersion: 1, +}; + +export const sampleStatusMap = { + http_ref: { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref", + retryCount: 0, + seq: 1, + pollCount: 1, + taskDefName: "http", + scheduledTime: 1713413856786, + startTime: 1713413856891, + endTime: 1713413856963, + updateTime: 1713413856891, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "965fb8d9-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": ["max-age=15724800; includeSubDomains"], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:36 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8678, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "mmxpwylvytawptmkxykq", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http", + taskReferenceName: "http_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + queueWaitTime: 105, + loopOverTask: false, + taskDefinition: null, + loopOver: [ + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref", + retryCount: 0, + seq: 1, + pollCount: 1, + taskDefName: "http", + scheduledTime: 1713413856786, + startTime: 1713413856891, + endTime: 1713413856963, + updateTime: 1713413856891, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "965fb8d9-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:36 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8678, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "mmxpwylvytawptmkxykq", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http", + taskReferenceName: "http_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + queueWaitTime: 105, + loopOverTask: false, + taskDefinition: null, + }, + ], + }, + do_while_ref: { + taskType: "DO_WHILE", + status: "COMPLETED", + inputData: { + number: 10, + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "do_while_ref", + retryCount: 0, + seq: 2, + pollCount: 0, + taskDefName: "do_while", + scheduledTime: 1713413856971, + startTime: 1713413856971, + endTime: 1713413859275, + updateTime: 1713413859060, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "967bcc5a-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + 1: { + inline_ref: { + result: 1, + }, + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_ref_3: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["180"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 47, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "vxqmzdyagxmexnpbaiqd", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7523, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "rzlozabxrltfkexdijot", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 2: { + http_ref_2: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 633, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "pnnnyucqifmchdazstau", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + inline_ref: { + result: 2, + }, + switch_ref: { + evaluationResult: ["2"], + selectedCase: "2", + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9295, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "bgnycyjkhkdmiqsnjpcm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 3: { + http_ref_1: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5570, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "sxopfwobgmlbyhectkue", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + inline_ref: { + result: 3, + }, + switch_ref: { + evaluationResult: ["3"], + selectedCase: "3", + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 1008, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "drzushsnuxfkfbxjqvfh", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 4: { + inline_ref: { + result: 1, + }, + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_ref_3: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 3964, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "qsrrgcyapbgeuoceizaa", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 6761, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "taupmlrpyhpzpsulaxqo", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 5: { + inline_ref: { + result: 1, + }, + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_ref_3: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 698, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "aobgjizxlwgiwdasfclh", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 6970, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "igwwnykfobqdnkxmafab", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 6: { + inline_ref: { + result: 1, + }, + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_ref_3: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2489, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "vbksjctcduvxcucqtykv", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8930, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "purhjvrqkogxpcyjifxv", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 7: { + http_ref_2: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 970, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "abxkbgqcvgvqnxjcishg", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + inline_ref: { + result: 2, + }, + switch_ref: { + evaluationResult: ["2"], + selectedCase: "2", + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5965, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "cozkeartchukblaomchw", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 8: { + http_ref_1: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5144, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "ffeeyciploflvzcqtrsc", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + inline_ref: { + result: 3, + }, + switch_ref: { + evaluationResult: ["3"], + selectedCase: "3", + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9726, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "qmkipozchmypxunkkkzd", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 9: { + inline_ref: { + result: 1, + }, + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_ref_3: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 1122, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "qixyannkpllogxldfyqi", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2780, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "jklxzsbanazwjffbarxi", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 10: { + http_ref_2: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7706, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "wadmrkumznvlcrfbfpoz", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + inline_ref: { + result: 2, + }, + switch_ref: { + evaluationResult: ["2"], + selectedCase: "2", + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 944, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "piubltpqkibleiqpmeno", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + iteration: 10, + }, + workflowTask: { + name: "do_while", + taskReferenceName: "do_while_ref", + inputParameters: { + number: 10, + }, + type: "DO_WHILE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopCondition: + "(function () {\n if ($.do_while_ref['iteration'] < $.number) {\n return true;\n }\n return false;\n})();", + loopOver: [ + { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + evaluatorType: "graaljs", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + loopOver: [ + { + taskType: "DO_WHILE", + status: "COMPLETED", + inputData: { + number: 10, + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "do_while_ref", + retryCount: 0, + seq: 2, + pollCount: 0, + taskDefName: "do_while", + scheduledTime: 1713413856971, + startTime: 1713413856971, + endTime: 1713413859275, + updateTime: 1713413859060, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "967bcc5a-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + 1: { + inline_ref: { + result: 1, + }, + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_ref_3: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["180"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 47, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "vxqmzdyagxmexnpbaiqd", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7523, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "rzlozabxrltfkexdijot", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 2: { + http_ref_2: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 633, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "pnnnyucqifmchdazstau", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + inline_ref: { + result: 2, + }, + switch_ref: { + evaluationResult: ["2"], + selectedCase: "2", + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9295, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "bgnycyjkhkdmiqsnjpcm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 3: { + http_ref_1: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5570, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "sxopfwobgmlbyhectkue", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + inline_ref: { + result: 3, + }, + switch_ref: { + evaluationResult: ["3"], + selectedCase: "3", + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 1008, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "drzushsnuxfkfbxjqvfh", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 4: { + inline_ref: { + result: 1, + }, + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_ref_3: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 3964, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "qsrrgcyapbgeuoceizaa", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 6761, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "taupmlrpyhpzpsulaxqo", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 5: { + inline_ref: { + result: 1, + }, + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_ref_3: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 698, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "aobgjizxlwgiwdasfclh", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 6970, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "igwwnykfobqdnkxmafab", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 6: { + inline_ref: { + result: 1, + }, + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_ref_3: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2489, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "vbksjctcduvxcucqtykv", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8930, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "purhjvrqkogxpcyjifxv", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 7: { + http_ref_2: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 970, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "abxkbgqcvgvqnxjcishg", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + inline_ref: { + result: 2, + }, + switch_ref: { + evaluationResult: ["2"], + selectedCase: "2", + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5965, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "cozkeartchukblaomchw", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 8: { + http_ref_1: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5144, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "ffeeyciploflvzcqtrsc", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + inline_ref: { + result: 3, + }, + switch_ref: { + evaluationResult: ["3"], + selectedCase: "3", + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9726, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "qmkipozchmypxunkkkzd", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 9: { + inline_ref: { + result: 1, + }, + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_ref_3: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 1122, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "qixyannkpllogxldfyqi", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2780, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "jklxzsbanazwjffbarxi", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 10: { + http_ref_2: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7706, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "wadmrkumznvlcrfbfpoz", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + inline_ref: { + result: 2, + }, + switch_ref: { + evaluationResult: ["2"], + selectedCase: "2", + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 944, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "piubltpqkibleiqpmeno", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + iteration: 10, + }, + workflowTask: { + name: "do_while", + taskReferenceName: "do_while_ref", + inputParameters: { + number: 10, + }, + type: "DO_WHILE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopCondition: + "(function () {\n if ($.do_while_ref['iteration'] < $.number) {\n return true;\n }\n return false;\n})();", + loopOver: [ + { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + evaluatorType: "graaljs", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + ], + }, + inline_ref: { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref__10", + retryCount: 0, + seq: 39, + pollCount: 0, + taskDefName: "inline", + scheduledTime: 1713413859056, + startTime: 1713413859056, + endTime: 1713413859075, + updateTime: 1713413859056, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97b9cabf-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 2, + }, + workflowTask: { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + loopOver: [ + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref__1", + retryCount: 0, + seq: 3, + pollCount: 0, + taskDefName: "inline", + scheduledTime: 1713413856975, + startTime: 1713413856975, + endTime: 1713413857004, + updateTime: 1713413856975, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "967c418b-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 1, + }, + workflowTask: { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref__2", + retryCount: 0, + seq: 7, + pollCount: 0, + taskDefName: "inline", + scheduledTime: 1713413857247, + startTime: 1713413857247, + endTime: 1713413857262, + updateTime: 1713413857247, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "96a5e99f-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 2, + }, + workflowTask: { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 2, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref__3", + retryCount: 0, + seq: 11, + pollCount: 0, + taskDefName: "inline", + scheduledTime: 1713413857465, + startTime: 1713413857465, + endTime: 1713413857480, + updateTime: 1713413857465, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "96c70633-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 3, + }, + workflowTask: { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 3, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref__4", + retryCount: 0, + seq: 15, + pollCount: 0, + taskDefName: "inline", + scheduledTime: 1713413857687, + startTime: 1713413857687, + endTime: 1713413857702, + updateTime: 1713413857687, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "96e90d27-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 1, + }, + workflowTask: { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 4, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref__5", + retryCount: 0, + seq: 19, + pollCount: 0, + taskDefName: "inline", + scheduledTime: 1713413857930, + startTime: 1713413857930, + endTime: 1713413857945, + updateTime: 1713413857930, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "970e215b-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 1, + }, + workflowTask: { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 5, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref__6", + retryCount: 0, + seq: 23, + pollCount: 0, + taskDefName: "inline", + scheduledTime: 1713413858154, + startTime: 1713413858154, + endTime: 1713413858196, + updateTime: 1713413858154, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97304f5f-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 1, + }, + workflowTask: { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 6, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref__7", + retryCount: 0, + seq: 27, + pollCount: 0, + taskDefName: "inline", + scheduledTime: 1713413858383, + startTime: 1713413858383, + endTime: 1713413858402, + updateTime: 1713413858383, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "975319a3-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 2, + }, + workflowTask: { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 7, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref__8", + retryCount: 0, + seq: 31, + pollCount: 0, + taskDefName: "inline", + scheduledTime: 1713413858598, + startTime: 1713413858598, + endTime: 1713413858615, + updateTime: 1713413858598, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9773e817-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 3, + }, + workflowTask: { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 8, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref__9", + retryCount: 0, + seq: 35, + pollCount: 0, + taskDefName: "inline", + scheduledTime: 1713413858836, + startTime: 1713413858836, + endTime: 1713413858852, + updateTime: 1713413858836, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "979811eb-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 1, + }, + workflowTask: { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 9, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref__10", + retryCount: 0, + seq: 39, + pollCount: 0, + taskDefName: "inline", + scheduledTime: 1713413859056, + startTime: 1713413859056, + endTime: 1713413859075, + updateTime: 1713413859056, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97b9cabf-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 2, + }, + workflowTask: { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + ], + }, + switch_ref: { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 2, + _createdBy: "najeeb.thangal@orkes.io", + case: "2", + }, + referenceTaskName: "switch_ref__10", + retryCount: 0, + seq: 40, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413859083, + startTime: 1713413859083, + endTime: 1713413859083, + updateTime: 1713413859083, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97bd7440-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["2"], + selectedCase: "2", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + loopOver: [ + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 1, + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref__1", + retryCount: 0, + seq: 4, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413857015, + startTime: 1713413857015, + endTime: 1713413857015, + updateTime: 1713413857015, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "968171ac-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 2, + _createdBy: "najeeb.thangal@orkes.io", + case: "2", + }, + referenceTaskName: "switch_ref__2", + retryCount: 0, + seq: 8, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413857270, + startTime: 1713413857270, + endTime: 1713413857270, + updateTime: 1713413857270, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "96a8f6e0-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["2"], + selectedCase: "2", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 2, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 3, + _createdBy: "najeeb.thangal@orkes.io", + case: "3", + }, + referenceTaskName: "switch_ref__3", + retryCount: 0, + seq: 12, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413857488, + startTime: 1713413857488, + endTime: 1713413857488, + updateTime: 1713413857488, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "96ca3a84-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["3"], + selectedCase: "3", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 3, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 1, + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref__4", + retryCount: 0, + seq: 16, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413857710, + startTime: 1713413857711, + endTime: 1713413857711, + updateTime: 1713413857711, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "96ebf358-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 4, + subworkflowChanged: false, + queueWaitTime: 1, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 1, + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref__5", + retryCount: 0, + seq: 20, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413857953, + startTime: 1713413857953, + endTime: 1713413857953, + updateTime: 1713413857953, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97112e9c-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 5, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 1, + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref__6", + retryCount: 0, + seq: 24, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413858204, + startTime: 1713413858204, + endTime: 1713413858204, + updateTime: 1713413858204, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97377b50-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 6, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 2, + _createdBy: "najeeb.thangal@orkes.io", + case: "2", + }, + referenceTaskName: "switch_ref__7", + retryCount: 0, + seq: 28, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413858412, + startTime: 1713413858412, + endTime: 1713413858412, + updateTime: 1713413858412, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97571144-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["2"], + selectedCase: "2", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 7, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 3, + _createdBy: "najeeb.thangal@orkes.io", + case: "3", + }, + referenceTaskName: "switch_ref__8", + retryCount: 0, + seq: 32, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413858625, + startTime: 1713413858625, + endTime: 1713413858625, + updateTime: 1713413858625, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97779198-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["3"], + selectedCase: "3", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 8, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 1, + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref__9", + retryCount: 0, + seq: 36, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413858864, + startTime: 1713413858864, + endTime: 1713413858864, + updateTime: 1713413858864, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "979c098c-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 9, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 2, + _createdBy: "najeeb.thangal@orkes.io", + case: "2", + }, + referenceTaskName: "switch_ref__10", + retryCount: 0, + seq: 40, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413859083, + startTime: 1713413859083, + endTime: 1713413859083, + updateTime: 1713413859083, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97bd7440-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["2"], + selectedCase: "2", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + ], + }, + http_ref_3: { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_3__9", + retryCount: 0, + seq: 37, + pollCount: 1, + taskDefName: "http_3", + scheduledTime: 1713413858858, + startTime: 1713413858917, + endTime: 1713413858928, + updateTime: 1713413858917, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "979c098d-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": ["max-age=15724800; includeSubDomains"], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 1122, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "qixyannkpllogxldfyqi", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 9, + subworkflowChanged: false, + queueWaitTime: 59, + loopOverTask: true, + taskDefinition: null, + loopOver: [ + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_3__1", + retryCount: 0, + seq: 5, + pollCount: 1, + taskDefName: "http_3", + scheduledTime: 1713413857007, + startTime: 1713413857108, + endTime: 1713413857122, + updateTime: 1713413857108, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "968171ad-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["180"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 47, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "vxqmzdyagxmexnpbaiqd", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 101, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_3__4", + retryCount: 0, + seq: 17, + pollCount: 1, + taskDefName: "http_3", + scheduledTime: 1713413857705, + startTime: 1713413857770, + endTime: 1713413857805, + updateTime: 1713413857770, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "96ec1a69-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 3964, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "qsrrgcyapbgeuoceizaa", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 4, + subworkflowChanged: false, + queueWaitTime: 65, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_3__5", + retryCount: 0, + seq: 21, + pollCount: 1, + taskDefName: "http_3", + scheduledTime: 1713413857948, + startTime: 1713413858015, + endTime: 1713413858027, + updateTime: 1713413858015, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97112e9d-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 698, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "aobgjizxlwgiwdasfclh", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 5, + subworkflowChanged: false, + queueWaitTime: 67, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_3__6", + retryCount: 0, + seq: 25, + pollCount: 1, + taskDefName: "http_3", + scheduledTime: 1713413858199, + startTime: 1713413858238, + endTime: 1713413858249, + updateTime: 1713413858238, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97377b51-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2489, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "vbksjctcduvxcucqtykv", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 6, + subworkflowChanged: false, + queueWaitTime: 39, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_3__9", + retryCount: 0, + seq: 37, + pollCount: 1, + taskDefName: "http_3", + scheduledTime: 1713413858858, + startTime: 1713413858917, + endTime: 1713413858928, + updateTime: 1713413858917, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "979c098d-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 1122, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "qixyannkpllogxldfyqi", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 9, + subworkflowChanged: false, + queueWaitTime: 59, + loopOverTask: true, + taskDefinition: null, + }, + ], + }, + http_ref_10: { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_10__10", + retryCount: 0, + seq: 42, + pollCount: 1, + taskDefName: "http_10", + scheduledTime: 1713413859158, + startTime: 1713413859250, + endTime: 1713413859260, + updateTime: 1713413859250, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97c9d052-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": ["max-age=15724800; includeSubDomains"], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 944, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "piubltpqkibleiqpmeno", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 92, + loopOverTask: true, + taskDefinition: null, + loopOver: [ + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_10__1", + retryCount: 0, + seq: 6, + pollCount: 1, + taskDefName: "http_10", + scheduledTime: 1713413857128, + startTime: 1713413857218, + endTime: 1713413857230, + updateTime: 1713413857218, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9693e83e-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7523, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "rzlozabxrltfkexdijot", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 90, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_10__2", + retryCount: 0, + seq: 10, + pollCount: 1, + taskDefName: "http_10", + scheduledTime: 1713413857348, + startTime: 1713413857439, + endTime: 1713413857451, + updateTime: 1713413857439, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "96b5a112-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9295, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "bgnycyjkhkdmiqsnjpcm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 2, + subworkflowChanged: false, + queueWaitTime: 91, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_10__3", + retryCount: 0, + seq: 14, + pollCount: 1, + taskDefName: "http_10", + scheduledTime: 1713413857567, + startTime: 1713413857659, + endTime: 1713413857671, + updateTime: 1713413857659, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "96d70bc6-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 1008, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "drzushsnuxfkfbxjqvfh", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 3, + subworkflowChanged: false, + queueWaitTime: 92, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_10__4", + retryCount: 0, + seq: 18, + pollCount: 1, + taskDefName: "http_10", + scheduledTime: 1713413857810, + startTime: 1713413857904, + endTime: 1713413857915, + updateTime: 1713413857904, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "96fc1ffa-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 6761, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "taupmlrpyhpzpsulaxqo", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 4, + subworkflowChanged: false, + queueWaitTime: 94, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_10__5", + retryCount: 0, + seq: 22, + pollCount: 1, + taskDefName: "http_10", + scheduledTime: 1713413858034, + startTime: 1713413858126, + endTime: 1713413858139, + updateTime: 1713413858126, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "971e4dfe-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 6970, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "igwwnykfobqdnkxmafab", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 5, + subworkflowChanged: false, + queueWaitTime: 92, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_10__6", + retryCount: 0, + seq: 26, + pollCount: 1, + taskDefName: "http_10", + scheduledTime: 1713413858256, + startTime: 1713413858349, + endTime: 1713413858361, + updateTime: 1713413858349, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97402de2-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8930, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "purhjvrqkogxpcyjifxv", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 6, + subworkflowChanged: false, + queueWaitTime: 93, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_10__7", + retryCount: 0, + seq: 30, + pollCount: 1, + taskDefName: "http_10", + scheduledTime: 1713413858478, + startTime: 1713413858571, + endTime: 1713413858583, + updateTime: 1713413858571, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97620dc6-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5965, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "cozkeartchukblaomchw", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 7, + subworkflowChanged: false, + queueWaitTime: 93, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_10__8", + retryCount: 0, + seq: 34, + pollCount: 1, + taskDefName: "http_10", + scheduledTime: 1713413858712, + startTime: 1713413858806, + endTime: 1713413858818, + updateTime: 1713413858806, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9785c26a-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9726, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "qmkipozchmypxunkkkzd", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 8, + subworkflowChanged: false, + queueWaitTime: 94, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_10__9", + retryCount: 0, + seq: 38, + pollCount: 1, + taskDefName: "http_10", + scheduledTime: 1713413858936, + startTime: 1713413859029, + endTime: 1713413859040, + updateTime: 1713413859029, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97a7c95e-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2780, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "jklxzsbanazwjffbarxi", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 9, + subworkflowChanged: false, + queueWaitTime: 93, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_10__10", + retryCount: 0, + seq: 42, + pollCount: 1, + taskDefName: "http_10", + scheduledTime: 1713413859158, + startTime: 1713413859250, + endTime: 1713413859260, + updateTime: 1713413859250, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97c9d052-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 944, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "piubltpqkibleiqpmeno", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 92, + loopOverTask: true, + taskDefinition: null, + }, + ], + }, + http_ref_2: { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_2__10", + retryCount: 0, + seq: 41, + pollCount: 1, + taskDefName: "http_2", + scheduledTime: 1713413859078, + startTime: 1713413859140, + endTime: 1713413859152, + updateTime: 1713413859140, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97bd7441-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": ["max-age=15724800; includeSubDomains"], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7706, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "wadmrkumznvlcrfbfpoz", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 62, + loopOverTask: true, + taskDefinition: null, + loopOver: [ + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_2__2", + retryCount: 0, + seq: 9, + pollCount: 1, + taskDefName: "http_2", + scheduledTime: 1713413857265, + startTime: 1713413857328, + endTime: 1713413857341, + updateTime: 1713413857329, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "96a8f6e1-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 633, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "pnnnyucqifmchdazstau", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 2, + subworkflowChanged: false, + queueWaitTime: 63, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_2__7", + retryCount: 0, + seq: 29, + pollCount: 1, + taskDefName: "http_2", + scheduledTime: 1713413858406, + startTime: 1713413858460, + endTime: 1713413858471, + updateTime: 1713413858460, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97571145-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 970, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "abxkbgqcvgvqnxjcishg", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 7, + subworkflowChanged: false, + queueWaitTime: 54, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_2__10", + retryCount: 0, + seq: 41, + pollCount: 1, + taskDefName: "http_2", + scheduledTime: 1713413859078, + startTime: 1713413859140, + endTime: 1713413859152, + updateTime: 1713413859140, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97bd7441-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7706, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "wadmrkumznvlcrfbfpoz", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 62, + loopOverTask: true, + taskDefinition: null, + }, + ], + }, + http_ref_1: { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_1__8", + retryCount: 0, + seq: 33, + pollCount: 1, + taskDefName: "http_1", + scheduledTime: 1713413858620, + startTime: 1713413858694, + endTime: 1713413858705, + updateTime: 1713413858694, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97779199-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": ["max-age=15724800; includeSubDomains"], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5144, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "ffeeyciploflvzcqtrsc", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 8, + subworkflowChanged: false, + queueWaitTime: 74, + loopOverTask: true, + taskDefinition: null, + loopOver: [ + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_1__3", + retryCount: 0, + seq: 13, + pollCount: 1, + taskDefName: "http_1", + scheduledTime: 1713413857483, + startTime: 1713413857549, + endTime: 1713413857561, + updateTime: 1713413857549, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "96ca3a85-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5570, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "sxopfwobgmlbyhectkue", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 3, + subworkflowChanged: false, + queueWaitTime: 66, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_1__8", + retryCount: 0, + seq: 33, + pollCount: 1, + taskDefName: "http_1", + scheduledTime: 1713413858620, + startTime: 1713413858694, + endTime: 1713413858705, + updateTime: 1713413858694, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97779199-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5144, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "ffeeyciploflvzcqtrsc", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 8, + subworkflowChanged: false, + queueWaitTime: 74, + loopOverTask: true, + taskDefinition: null, + }, + ], + }, + http_ref_4: { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_4", + retryCount: 0, + seq: 43, + pollCount: 1, + taskDefName: "http_4", + scheduledTime: 1713413859277, + startTime: 1713413859361, + endTime: 1713413859373, + updateTime: 1713413859361, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97dbf8c3-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": ["max-age=15724800; includeSubDomains"], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5081, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "yfxgvcpjtxnjlftbtcpr", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_4", + taskReferenceName: "http_ref_4", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + queueWaitTime: 84, + loopOverTask: false, + taskDefinition: null, + loopOver: [ + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_4", + retryCount: 0, + seq: 43, + pollCount: 1, + taskDefName: "http_4", + scheduledTime: 1713413859277, + startTime: 1713413859361, + endTime: 1713413859373, + updateTime: 1713413859361, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97dbf8c3-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5081, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "yfxgvcpjtxnjlftbtcpr", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_4", + taskReferenceName: "http_ref_4", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + queueWaitTime: 84, + loopOverTask: false, + taskDefinition: null, + }, + ], + }, + do_while_ref_1: { + taskType: "DO_WHILE", + status: "COMPLETED", + inputData: { + number: 10, + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "do_while_ref_1", + retryCount: 0, + seq: 44, + pollCount: 0, + taskDefName: "do_while_1", + scheduledTime: 1713413859382, + startTime: 1713413859382, + endTime: 1713413862160, + updateTime: 1713413861832, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97eb8924-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + 1: { + switch_ref_1: { + evaluationResult: ["2"], + selectedCase: "2", + }, + inline_ref_1: { + result: 2, + }, + http_ref_7: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9943, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "tjxztrsclyzabbqspjmv", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9428, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "lwfynuupwbgkcwgqemcs", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 2: { + switch_ref_1: { + evaluationResult: ["3"], + selectedCase: "3", + }, + inline_ref_1: { + result: 3, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 3723, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "kjmdqmvozujqsxupnlot", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 154, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "obdruesgtvezidzuenhu", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 1892, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "heqrbsinaexemndlvmcp", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 3: { + switch_ref_1: { + evaluationResult: ["1"], + selectedCase: "1", + }, + inline_ref_1: { + result: 1, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7080, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "cuurineeimzdpveetkte", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9931, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "ktrbhjusvnttizwmopxg", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 3569, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "rbeetiujrcnurymrqpvl", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 4: { + switch_ref_1: { + evaluationResult: ["3"], + selectedCase: "3", + }, + inline_ref_1: { + result: 3, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8155, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "tniyursobyqwmcgnfxcj", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5254, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "fqlewtggkleyvghxoyqd", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7061, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "ixireilwipqxtseivuxm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 5: { + switch_ref_1: { + evaluationResult: ["1"], + selectedCase: "1", + }, + inline_ref_1: { + result: 1, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5878, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "bksldazxfxhyoyefvrxf", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2932, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "djokkjixbuwxlsstoaab", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9316, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "dytnpkdddlzmtzlwfpal", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 6: { + switch_ref_1: { + evaluationResult: ["4"], + selectedCase: "4", + }, + inline_ref_1: { + result: 4, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4089, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "khrlsfsjmhvsjgtshbfz", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 7: { + switch_ref_1: { + evaluationResult: ["4"], + selectedCase: "4", + }, + inline_ref_1: { + result: 4, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8433, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "nwltborbttslioxepwhe", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 8: { + switch_ref_1: { + evaluationResult: ["1"], + selectedCase: "1", + }, + inline_ref_1: { + result: 1, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 242, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "idkvujpflbawjvbtcqol", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5914, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "sccygcowawimarjtitnq", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2173, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "saxapbpahdmmtpavvjlm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 9: { + switch_ref_1: { + evaluationResult: ["1"], + selectedCase: "1", + }, + inline_ref_1: { + result: 1, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4359, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "lmbrmrnefvkwwwbmvolm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 395, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "myxiwwnhjbxhdwkfwmzm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2885, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "ykuzzhmdpvfcragcwxoh", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 10: { + switch_ref_1: { + evaluationResult: ["3"], + selectedCase: "3", + }, + inline_ref_1: { + result: 3, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:42 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4274, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "kwmoumdlucxuwgphkxaj", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5175, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "enhhdgrjmtgntufhymzm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:42 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4750, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "oeenqbhrzifhhetyzpcu", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + iteration: 10, + }, + workflowTask: { + name: "do_while_1", + taskReferenceName: "do_while_ref_1", + inputParameters: { + number: 10, + }, + type: "DO_WHILE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopCondition: + "(function () {\n if ($.do_while_ref_1['iteration'] < $.number) {\n return true;\n }\n return false;\n})();", + loopOver: [ + { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + evaluatorType: "graaljs", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + loopOver: [ + { + taskType: "DO_WHILE", + status: "COMPLETED", + inputData: { + number: 10, + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "do_while_ref_1", + retryCount: 0, + seq: 44, + pollCount: 0, + taskDefName: "do_while_1", + scheduledTime: 1713413859382, + startTime: 1713413859382, + endTime: 1713413862160, + updateTime: 1713413861832, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97eb8924-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + 1: { + switch_ref_1: { + evaluationResult: ["2"], + selectedCase: "2", + }, + inline_ref_1: { + result: 2, + }, + http_ref_7: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9943, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "tjxztrsclyzabbqspjmv", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9428, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "lwfynuupwbgkcwgqemcs", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 2: { + switch_ref_1: { + evaluationResult: ["3"], + selectedCase: "3", + }, + inline_ref_1: { + result: 3, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 3723, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "kjmdqmvozujqsxupnlot", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 154, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "obdruesgtvezidzuenhu", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 1892, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "heqrbsinaexemndlvmcp", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 3: { + switch_ref_1: { + evaluationResult: ["1"], + selectedCase: "1", + }, + inline_ref_1: { + result: 1, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7080, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "cuurineeimzdpveetkte", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9931, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "ktrbhjusvnttizwmopxg", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 3569, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "rbeetiujrcnurymrqpvl", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 4: { + switch_ref_1: { + evaluationResult: ["3"], + selectedCase: "3", + }, + inline_ref_1: { + result: 3, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8155, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "tniyursobyqwmcgnfxcj", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5254, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "fqlewtggkleyvghxoyqd", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7061, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "ixireilwipqxtseivuxm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 5: { + switch_ref_1: { + evaluationResult: ["1"], + selectedCase: "1", + }, + inline_ref_1: { + result: 1, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5878, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "bksldazxfxhyoyefvrxf", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2932, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "djokkjixbuwxlsstoaab", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9316, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "dytnpkdddlzmtzlwfpal", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 6: { + switch_ref_1: { + evaluationResult: ["4"], + selectedCase: "4", + }, + inline_ref_1: { + result: 4, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4089, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "khrlsfsjmhvsjgtshbfz", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 7: { + switch_ref_1: { + evaluationResult: ["4"], + selectedCase: "4", + }, + inline_ref_1: { + result: 4, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8433, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "nwltborbttslioxepwhe", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 8: { + switch_ref_1: { + evaluationResult: ["1"], + selectedCase: "1", + }, + inline_ref_1: { + result: 1, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 242, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "idkvujpflbawjvbtcqol", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5914, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "sccygcowawimarjtitnq", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2173, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "saxapbpahdmmtpavvjlm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 9: { + switch_ref_1: { + evaluationResult: ["1"], + selectedCase: "1", + }, + inline_ref_1: { + result: 1, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4359, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "lmbrmrnefvkwwwbmvolm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 395, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "myxiwwnhjbxhdwkfwmzm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2885, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "ykuzzhmdpvfcragcwxoh", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 10: { + switch_ref_1: { + evaluationResult: ["3"], + selectedCase: "3", + }, + inline_ref_1: { + result: 3, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:42 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4274, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "kwmoumdlucxuwgphkxaj", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5175, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "enhhdgrjmtgntufhymzm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:42 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4750, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "oeenqbhrzifhhetyzpcu", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + iteration: 10, + }, + workflowTask: { + name: "do_while_1", + taskReferenceName: "do_while_ref_1", + inputParameters: { + number: 10, + }, + type: "DO_WHILE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopCondition: + "(function () {\n if ($.do_while_ref_1['iteration'] < $.number) {\n return true;\n }\n return false;\n})();", + loopOver: [ + { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + evaluatorType: "graaljs", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + ], + }, + inline_ref_1: { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref_1__10", + retryCount: 0, + seq: 85, + pollCount: 0, + taskDefName: "inline_1", + scheduledTime: 1713413861829, + startTime: 1713413861829, + endTime: 1713413861842, + updateTime: 1713413861829, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9960760d-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 3, + }, + workflowTask: { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + loopOver: [ + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref_1__1", + retryCount: 0, + seq: 45, + pollCount: 0, + taskDefName: "inline_1", + scheduledTime: 1713413859386, + startTime: 1713413859386, + endTime: 1713413859402, + updateTime: 1713413859386, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97ec2565-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 2, + }, + workflowTask: { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref_1__2", + retryCount: 0, + seq: 49, + pollCount: 0, + taskDefName: "inline_1", + scheduledTime: 1713413859617, + startTime: 1713413859617, + endTime: 1713413859629, + updateTime: 1713413859617, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "980f64d9-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 3, + }, + workflowTask: { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 2, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref_1__3", + retryCount: 0, + seq: 54, + pollCount: 0, + taskDefName: "inline_1", + scheduledTime: 1713413859945, + startTime: 1713413859945, + endTime: 1713413859959, + updateTime: 1713413859945, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9841715e-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 1, + }, + workflowTask: { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 3, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref_1__4", + retryCount: 0, + seq: 59, + pollCount: 0, + taskDefName: "inline_1", + scheduledTime: 1713413860274, + startTime: 1713413860274, + endTime: 1713413860287, + updateTime: 1713413860274, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9873a4f3-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 3, + }, + workflowTask: { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 4, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref_1__5", + retryCount: 0, + seq: 64, + pollCount: 0, + taskDefName: "inline_1", + scheduledTime: 1713413860607, + startTime: 1713413860607, + endTime: 1713413860621, + updateTime: 1713413860607, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98a69bd8-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 1, + }, + workflowTask: { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 5, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref_1__6", + retryCount: 0, + seq: 69, + pollCount: 0, + taskDefName: "inline_1", + scheduledTime: 1713413860941, + startTime: 1713413860941, + endTime: 1713413860957, + updateTime: 1713413860941, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98d96bad-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 4, + }, + workflowTask: { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 6, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref_1__7", + retryCount: 0, + seq: 72, + pollCount: 0, + taskDefName: "inline_1", + scheduledTime: 1713413861052, + startTime: 1713413861052, + endTime: 1713413861070, + updateTime: 1713413861052, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98ea3490-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 4, + }, + workflowTask: { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 7, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref_1__8", + retryCount: 0, + seq: 75, + pollCount: 0, + taskDefName: "inline_1", + scheduledTime: 1713413861161, + startTime: 1713413861161, + endTime: 1713413861175, + updateTime: 1713413861161, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98fafd73-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 1, + }, + workflowTask: { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 8, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref_1__9", + retryCount: 0, + seq: 80, + pollCount: 0, + taskDefName: "inline_1", + scheduledTime: 1713413861496, + startTime: 1713413861497, + endTime: 1713413861510, + updateTime: 1713413861497, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "992e4278-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 1, + }, + workflowTask: { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 9, + subworkflowChanged: false, + queueWaitTime: 1, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref_1__10", + retryCount: 0, + seq: 85, + pollCount: 0, + taskDefName: "inline_1", + scheduledTime: 1713413861829, + startTime: 1713413861829, + endTime: 1713413861842, + updateTime: 1713413861829, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9960760d-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 3, + }, + workflowTask: { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + ], + }, + switch_ref_1: { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 3, + _createdBy: "najeeb.thangal@orkes.io", + case: "3", + }, + referenceTaskName: "switch_ref_1__10", + retryCount: 0, + seq: 86, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413861851, + startTime: 1713413861851, + endTime: 1713413861851, + updateTime: 1713413861851, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9963d16e-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["3"], + selectedCase: "3", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + loopOver: [ + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 2, + _createdBy: "najeeb.thangal@orkes.io", + case: "2", + }, + referenceTaskName: "switch_ref_1__1", + retryCount: 0, + seq: 46, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413859413, + startTime: 1713413859413, + endTime: 1713413859413, + updateTime: 1713413859413, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97ef80c6-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["2"], + selectedCase: "2", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 3, + _createdBy: "najeeb.thangal@orkes.io", + case: "3", + }, + referenceTaskName: "switch_ref_1__2", + retryCount: 0, + seq: 50, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413859637, + startTime: 1713413859637, + endTime: 1713413859637, + updateTime: 1713413859637, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9811fcea-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["3"], + selectedCase: "3", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 2, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 1, + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref_1__3", + retryCount: 0, + seq: 55, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413859968, + startTime: 1713413859968, + endTime: 1713413859968, + updateTime: 1713413859968, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98447e9f-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 3, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 3, + _createdBy: "najeeb.thangal@orkes.io", + case: "3", + }, + referenceTaskName: "switch_ref_1__4", + retryCount: 0, + seq: 60, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413860295, + startTime: 1713413860295, + endTime: 1713413860295, + updateTime: 1713413860295, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98768b24-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["3"], + selectedCase: "3", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 4, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 1, + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref_1__5", + retryCount: 0, + seq: 65, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413860629, + startTime: 1713413860629, + endTime: 1713413860629, + updateTime: 1713413860629, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98a98209-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 5, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + switchCaseValue: 4, + _createdBy: "najeeb.thangal@orkes.io", + case: "4", + }, + referenceTaskName: "switch_ref_1__6", + retryCount: 0, + seq: 70, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413860963, + startTime: 1713413860963, + endTime: 1713413860963, + updateTime: 1713413860963, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98dcc70e-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["4"], + selectedCase: "4", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 6, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + switchCaseValue: 4, + _createdBy: "najeeb.thangal@orkes.io", + case: "4", + }, + referenceTaskName: "switch_ref_1__7", + retryCount: 0, + seq: 73, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413861076, + startTime: 1713413861076, + endTime: 1713413861076, + updateTime: 1713413861076, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98ee0521-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["4"], + selectedCase: "4", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 7, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 1, + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref_1__8", + retryCount: 0, + seq: 76, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413861183, + startTime: 1713413861183, + endTime: 1713413861183, + updateTime: 1713413861183, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98fe0ab4-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 8, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 1, + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref_1__9", + retryCount: 0, + seq: 81, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413861520, + startTime: 1713413861520, + endTime: 1713413861520, + updateTime: 1713413861520, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "993128a9-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 9, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 3, + _createdBy: "najeeb.thangal@orkes.io", + case: "3", + }, + referenceTaskName: "switch_ref_1__10", + retryCount: 0, + seq: 86, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413861851, + startTime: 1713413861851, + endTime: 1713413861851, + updateTime: 1713413861851, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9963d16e-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["3"], + selectedCase: "3", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + ], + }, + http_ref_7: { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_7__1", + retryCount: 0, + seq: 47, + pollCount: 1, + taskDefName: "http_7", + scheduledTime: 1713413859406, + startTime: 1713413859472, + endTime: 1713413859483, + updateTime: 1713413859472, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97efa7d7-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": ["max-age=15724800; includeSubDomains"], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9943, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "tjxztrsclyzabbqspjmv", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 66, + loopOverTask: true, + taskDefinition: null, + loopOver: [ + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_7__1", + retryCount: 0, + seq: 47, + pollCount: 1, + taskDefName: "http_7", + scheduledTime: 1713413859406, + startTime: 1713413859472, + endTime: 1713413859483, + updateTime: 1713413859472, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97efa7d7-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9943, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "tjxztrsclyzabbqspjmv", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 66, + loopOverTask: true, + taskDefinition: null, + }, + ], + }, + http_ref_8: { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_8__10", + retryCount: 0, + seq: 89, + pollCount: 1, + taskDefName: "http_8", + scheduledTime: 1713413862045, + startTime: 1713413862132, + endTime: 1713413862145, + updateTime: 1713413862132, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "998255f1-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": ["max-age=15724800; includeSubDomains"], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:42 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4274, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "kwmoumdlucxuwgphkxaj", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 87, + loopOverTask: true, + taskDefinition: null, + loopOver: [ + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_8__1", + retryCount: 0, + seq: 48, + pollCount: 1, + taskDefName: "http_8", + scheduledTime: 1713413859490, + startTime: 1713413859583, + endTime: 1713413859594, + updateTime: 1713413859583, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97fc7918-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9428, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "lwfynuupwbgkcwgqemcs", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 93, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_8__2", + retryCount: 0, + seq: 53, + pollCount: 1, + taskDefName: "http_8", + scheduledTime: 1713413859823, + startTime: 1713413859916, + endTime: 1713413859927, + updateTime: 1713413859916, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "982f48ed-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 3723, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "kjmdqmvozujqsxupnlot", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 2, + subworkflowChanged: false, + queueWaitTime: 93, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_8__3", + retryCount: 0, + seq: 58, + pollCount: 1, + taskDefName: "http_8", + scheduledTime: 1713413860156, + startTime: 1713413860248, + endTime: 1713413860259, + updateTime: 1713413860248, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "986218c2-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7080, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "cuurineeimzdpveetkte", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 3, + subworkflowChanged: false, + queueWaitTime: 92, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_8__4", + retryCount: 0, + seq: 63, + pollCount: 1, + taskDefName: "http_8", + scheduledTime: 1713413860487, + startTime: 1713413860580, + endTime: 1713413860592, + updateTime: 1713413860580, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98949a77-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8155, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "tniyursobyqwmcgnfxcj", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 4, + subworkflowChanged: false, + queueWaitTime: 93, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_8__5", + retryCount: 0, + seq: 68, + pollCount: 1, + taskDefName: "http_8", + scheduledTime: 1713413860819, + startTime: 1713413860914, + endTime: 1713413860925, + updateTime: 1713413860914, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98c7433c-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5878, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "bksldazxfxhyoyefvrxf", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 5, + subworkflowChanged: false, + queueWaitTime: 95, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_8__6", + retryCount: 0, + seq: 71, + pollCount: 1, + taskDefName: "http_8", + scheduledTime: 1713413860970, + startTime: 1713413861025, + endTime: 1713413861036, + updateTime: 1713413861025, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98de269f-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4089, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "khrlsfsjmhvsjgtshbfz", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 6, + subworkflowChanged: false, + queueWaitTime: 55, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_8__7", + retryCount: 0, + seq: 74, + pollCount: 1, + taskDefName: "http_8", + scheduledTime: 1713413861082, + startTime: 1713413861136, + endTime: 1713413861147, + updateTime: 1713413861136, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98ef64b2-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8433, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "nwltborbttslioxepwhe", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 7, + subworkflowChanged: false, + queueWaitTime: 54, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_8__8", + retryCount: 0, + seq: 79, + pollCount: 1, + taskDefName: "http_8", + scheduledTime: 1713413861375, + startTime: 1713413861467, + endTime: 1713413861478, + updateTime: 1713413861467, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "991c1a07-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 242, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "idkvujpflbawjvbtcqol", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 8, + subworkflowChanged: false, + queueWaitTime: 92, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_8__9", + retryCount: 0, + seq: 84, + pollCount: 1, + taskDefName: "http_8", + scheduledTime: 1713413861704, + startTime: 1713413861798, + endTime: 1713413861810, + updateTime: 1713413861798, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "994e4d9c-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4359, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "lmbrmrnefvkwwwbmvolm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 9, + subworkflowChanged: false, + queueWaitTime: 94, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_8__10", + retryCount: 0, + seq: 89, + pollCount: 1, + taskDefName: "http_8", + scheduledTime: 1713413862045, + startTime: 1713413862132, + endTime: 1713413862145, + updateTime: 1713413862132, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "998255f1-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:42 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4274, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "kwmoumdlucxuwgphkxaj", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 87, + loopOverTask: true, + taskDefinition: null, + }, + ], + }, + http_ref_5: { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_5__10", + retryCount: 0, + seq: 87, + pollCount: 1, + taskDefName: "http_5", + scheduledTime: 1713413861845, + startTime: 1713413861911, + endTime: 1713413861920, + updateTime: 1713413861911, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9963d16f-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": ["max-age=15724800; includeSubDomains"], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5175, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "enhhdgrjmtgntufhymzm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 66, + loopOverTask: true, + taskDefinition: null, + loopOver: [ + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_5__2", + retryCount: 0, + seq: 51, + pollCount: 1, + taskDefName: "http_5", + scheduledTime: 1713413859632, + startTime: 1713413859695, + endTime: 1713413859706, + updateTime: 1713413859695, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "981223fb-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 154, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "obdruesgtvezidzuenhu", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 2, + subworkflowChanged: false, + queueWaitTime: 63, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_5__3", + retryCount: 0, + seq: 56, + pollCount: 1, + taskDefName: "http_5", + scheduledTime: 1713413859962, + startTime: 1713413860027, + endTime: 1713413860038, + updateTime: 1713413860027, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98447ea0-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9931, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "ktrbhjusvnttizwmopxg", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 3, + subworkflowChanged: false, + queueWaitTime: 65, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_5__4", + retryCount: 0, + seq: 61, + pollCount: 1, + taskDefName: "http_5", + scheduledTime: 1713413860290, + startTime: 1713413860358, + endTime: 1713413860370, + updateTime: 1713413860358, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98768b25-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5254, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "fqlewtggkleyvghxoyqd", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 4, + subworkflowChanged: false, + queueWaitTime: 68, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_5__5", + retryCount: 0, + seq: 66, + pollCount: 1, + taskDefName: "http_5", + scheduledTime: 1713413860624, + startTime: 1713413860692, + endTime: 1713413860703, + updateTime: 1713413860692, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98a9820a-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2932, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "djokkjixbuwxlsstoaab", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 5, + subworkflowChanged: false, + queueWaitTime: 68, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_5__8", + retryCount: 0, + seq: 77, + pollCount: 1, + taskDefName: "http_5", + scheduledTime: 1713413861178, + startTime: 1713413861247, + endTime: 1713413861257, + updateTime: 1713413861247, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98fe0ab5-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5914, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "sccygcowawimarjtitnq", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 8, + subworkflowChanged: false, + queueWaitTime: 69, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_5__9", + retryCount: 0, + seq: 82, + pollCount: 1, + taskDefName: "http_5", + scheduledTime: 1713413861514, + startTime: 1713413861577, + endTime: 1713413861588, + updateTime: 1713413861577, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "99314fba-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 395, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "myxiwwnhjbxhdwkfwmzm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 9, + subworkflowChanged: false, + queueWaitTime: 63, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_5__10", + retryCount: 0, + seq: 87, + pollCount: 1, + taskDefName: "http_5", + scheduledTime: 1713413861845, + startTime: 1713413861911, + endTime: 1713413861920, + updateTime: 1713413861911, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9963d16f-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5175, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "enhhdgrjmtgntufhymzm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 66, + loopOverTask: true, + taskDefinition: null, + }, + ], + }, + http_ref_6: { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_6__10", + retryCount: 0, + seq: 88, + pollCount: 1, + taskDefName: "http_6", + scheduledTime: 1713413861927, + startTime: 1713413862022, + endTime: 1713413862033, + updateTime: 1713413862022, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "99705490-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": ["max-age=15724800; includeSubDomains"], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:42 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4750, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "oeenqbhrzifhhetyzpcu", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 95, + loopOverTask: true, + taskDefinition: null, + loopOver: [ + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_6__2", + retryCount: 0, + seq: 52, + pollCount: 1, + taskDefName: "http_6", + scheduledTime: 1713413859717, + startTime: 1713413859806, + endTime: 1713413859816, + updateTime: 1713413859806, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "981f1c4c-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 1892, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "heqrbsinaexemndlvmcp", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 2, + subworkflowChanged: false, + queueWaitTime: 89, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_6__3", + retryCount: 0, + seq: 57, + pollCount: 1, + taskDefName: "http_6", + scheduledTime: 1713413860044, + startTime: 1713413860138, + endTime: 1713413860150, + updateTime: 1713413860138, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "985101c1-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 3569, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "rbeetiujrcnurymrqpvl", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 3, + subworkflowChanged: false, + queueWaitTime: 94, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_6__4", + retryCount: 0, + seq: 62, + pollCount: 1, + taskDefName: "http_6", + scheduledTime: 1713413860376, + startTime: 1713413860469, + endTime: 1713413860480, + updateTime: 1713413860469, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9883aa86-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7061, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "ixireilwipqxtseivuxm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 4, + subworkflowChanged: false, + queueWaitTime: 93, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_6__5", + retryCount: 0, + seq: 67, + pollCount: 1, + taskDefName: "http_6", + scheduledTime: 1713413860709, + startTime: 1713413860803, + endTime: 1713413860813, + updateTime: 1713413860803, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98b67a5b-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9316, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "dytnpkdddlzmtzlwfpal", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 5, + subworkflowChanged: false, + queueWaitTime: 94, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_6__8", + retryCount: 0, + seq: 78, + pollCount: 1, + taskDefName: "http_6", + scheduledTime: 1713413861263, + startTime: 1713413861357, + endTime: 1713413861368, + updateTime: 1713413861357, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "990b0306-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2173, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "saxapbpahdmmtpavvjlm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 8, + subworkflowChanged: false, + queueWaitTime: 94, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_6__9", + retryCount: 0, + seq: 83, + pollCount: 1, + taskDefName: "http_6", + scheduledTime: 1713413861594, + startTime: 1713413861688, + endTime: 1713413861698, + updateTime: 1713413861688, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "993d84bb-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2885, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "ykuzzhmdpvfcragcwxoh", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 9, + subworkflowChanged: false, + queueWaitTime: 94, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_6__10", + retryCount: 0, + seq: 88, + pollCount: 1, + taskDefName: "http_6", + scheduledTime: 1713413861927, + startTime: 1713413862022, + endTime: 1713413862033, + updateTime: 1713413862022, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "99705490-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:42 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4750, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "oeenqbhrzifhhetyzpcu", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 95, + loopOverTask: true, + taskDefinition: null, + }, + ], + }, + http_ref_9: { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_9", + retryCount: 0, + seq: 90, + pollCount: 1, + taskDefName: "http_9", + scheduledTime: 1713413862163, + startTime: 1713413862244, + endTime: 1713413862258, + updateTime: 1713413862244, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "99945752-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": ["max-age=15724800; includeSubDomains"], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:42 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2042, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "xwadanvnxdolxjqnchsa", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_9", + taskReferenceName: "http_ref_9", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + queueWaitTime: 81, + loopOverTask: false, + taskDefinition: null, + loopOver: [ + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_9", + retryCount: 0, + seq: 90, + pollCount: 1, + taskDefName: "http_9", + scheduledTime: 1713413862163, + startTime: 1713413862244, + endTime: 1713413862258, + updateTime: 1713413862244, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "99945752-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:42 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2042, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "xwadanvnxdolxjqnchsa", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_9", + taskReferenceName: "http_ref_9", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + queueWaitTime: 81, + loopOverTask: false, + taskDefinition: null, + }, + ], + }, +}; + +export const doWhileSelectionForStatusMapResultSingleDoWhile = { + http_ref: { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref", + retryCount: 0, + seq: 1, + pollCount: 1, + taskDefName: "http", + scheduledTime: 1713413856786, + startTime: 1713413856891, + endTime: 1713413856963, + updateTime: 1713413856891, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "965fb8d9-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": ["max-age=15724800; includeSubDomains"], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:36 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8678, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "mmxpwylvytawptmkxykq", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http", + taskReferenceName: "http_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + queueWaitTime: 105, + loopOverTask: false, + taskDefinition: null, + loopOver: [ + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref", + retryCount: 0, + seq: 1, + pollCount: 1, + taskDefName: "http", + scheduledTime: 1713413856786, + startTime: 1713413856891, + endTime: 1713413856963, + updateTime: 1713413856891, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "965fb8d9-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:36 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8678, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "mmxpwylvytawptmkxykq", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http", + taskReferenceName: "http_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + queueWaitTime: 105, + loopOverTask: false, + taskDefinition: null, + }, + ], + }, + do_while_ref: { + taskType: "DO_WHILE", + status: "COMPLETED", + inputData: { + number: 10, + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "do_while_ref", + retryCount: 0, + seq: 2, + pollCount: 0, + taskDefName: "do_while", + scheduledTime: 1713413856971, + startTime: 1713413856971, + endTime: 1713413859275, + updateTime: 1713413859060, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "967bcc5a-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + 1: { + inline_ref: { + result: 1, + }, + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_ref_3: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["180"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 47, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "vxqmzdyagxmexnpbaiqd", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7523, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "rzlozabxrltfkexdijot", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 2: { + http_ref_2: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 633, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "pnnnyucqifmchdazstau", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + inline_ref: { + result: 2, + }, + switch_ref: { + evaluationResult: ["2"], + selectedCase: "2", + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9295, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "bgnycyjkhkdmiqsnjpcm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 3: { + http_ref_1: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5570, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "sxopfwobgmlbyhectkue", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + inline_ref: { + result: 3, + }, + switch_ref: { + evaluationResult: ["3"], + selectedCase: "3", + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 1008, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "drzushsnuxfkfbxjqvfh", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 4: { + inline_ref: { + result: 1, + }, + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_ref_3: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 3964, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "qsrrgcyapbgeuoceizaa", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 6761, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "taupmlrpyhpzpsulaxqo", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 5: { + inline_ref: { + result: 1, + }, + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_ref_3: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 698, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "aobgjizxlwgiwdasfclh", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 6970, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "igwwnykfobqdnkxmafab", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 6: { + inline_ref: { + result: 1, + }, + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_ref_3: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2489, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "vbksjctcduvxcucqtykv", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8930, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "purhjvrqkogxpcyjifxv", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 7: { + http_ref_2: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 970, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "abxkbgqcvgvqnxjcishg", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + inline_ref: { + result: 2, + }, + switch_ref: { + evaluationResult: ["2"], + selectedCase: "2", + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5965, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "cozkeartchukblaomchw", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 8: { + http_ref_1: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5144, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "ffeeyciploflvzcqtrsc", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + inline_ref: { + result: 3, + }, + switch_ref: { + evaluationResult: ["3"], + selectedCase: "3", + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9726, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "qmkipozchmypxunkkkzd", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 9: { + inline_ref: { + result: 1, + }, + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_ref_3: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 1122, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "qixyannkpllogxldfyqi", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2780, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "jklxzsbanazwjffbarxi", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 10: { + http_ref_2: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7706, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "wadmrkumznvlcrfbfpoz", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + inline_ref: { + result: 2, + }, + switch_ref: { + evaluationResult: ["2"], + selectedCase: "2", + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 944, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "piubltpqkibleiqpmeno", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + iteration: 10, + }, + workflowTask: { + name: "do_while", + taskReferenceName: "do_while_ref", + inputParameters: { + number: 10, + }, + type: "DO_WHILE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopCondition: + "(function () {\n if ($.do_while_ref['iteration'] < $.number) {\n return true;\n }\n return false;\n})();", + loopOver: [ + { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + evaluatorType: "graaljs", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + loopOver: [ + { + taskType: "DO_WHILE", + status: "COMPLETED", + inputData: { + number: 10, + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "do_while_ref", + retryCount: 0, + seq: 2, + pollCount: 0, + taskDefName: "do_while", + scheduledTime: 1713413856971, + startTime: 1713413856971, + endTime: 1713413859275, + updateTime: 1713413859060, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "967bcc5a-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + 1: { + inline_ref: { + result: 1, + }, + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_ref_3: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["180"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 47, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "vxqmzdyagxmexnpbaiqd", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7523, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "rzlozabxrltfkexdijot", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 2: { + http_ref_2: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 633, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "pnnnyucqifmchdazstau", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + inline_ref: { + result: 2, + }, + switch_ref: { + evaluationResult: ["2"], + selectedCase: "2", + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9295, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "bgnycyjkhkdmiqsnjpcm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 3: { + http_ref_1: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5570, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "sxopfwobgmlbyhectkue", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + inline_ref: { + result: 3, + }, + switch_ref: { + evaluationResult: ["3"], + selectedCase: "3", + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 1008, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "drzushsnuxfkfbxjqvfh", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 4: { + inline_ref: { + result: 1, + }, + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_ref_3: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 3964, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "qsrrgcyapbgeuoceizaa", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 6761, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "taupmlrpyhpzpsulaxqo", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 5: { + inline_ref: { + result: 1, + }, + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_ref_3: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 698, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "aobgjizxlwgiwdasfclh", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 6970, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "igwwnykfobqdnkxmafab", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 6: { + inline_ref: { + result: 1, + }, + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_ref_3: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2489, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "vbksjctcduvxcucqtykv", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8930, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "purhjvrqkogxpcyjifxv", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 7: { + http_ref_2: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 970, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "abxkbgqcvgvqnxjcishg", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + inline_ref: { + result: 2, + }, + switch_ref: { + evaluationResult: ["2"], + selectedCase: "2", + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5965, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "cozkeartchukblaomchw", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 8: { + http_ref_1: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5144, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "ffeeyciploflvzcqtrsc", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + inline_ref: { + result: 3, + }, + switch_ref: { + evaluationResult: ["3"], + selectedCase: "3", + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9726, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "qmkipozchmypxunkkkzd", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 9: { + inline_ref: { + result: 1, + }, + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_ref_3: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 1122, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "qixyannkpllogxldfyqi", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2780, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "jklxzsbanazwjffbarxi", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 10: { + http_ref_2: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7706, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "wadmrkumznvlcrfbfpoz", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + inline_ref: { + result: 2, + }, + switch_ref: { + evaluationResult: ["2"], + selectedCase: "2", + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 944, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "piubltpqkibleiqpmeno", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + iteration: 10, + }, + workflowTask: { + name: "do_while", + taskReferenceName: "do_while_ref", + inputParameters: { + number: 10, + }, + type: "DO_WHILE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopCondition: + "(function () {\n if ($.do_while_ref['iteration'] < $.number) {\n return true;\n }\n return false;\n})();", + loopOver: [ + { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + evaluatorType: "graaljs", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + ], + }, + inline_ref: { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref__1", + retryCount: 0, + seq: 3, + pollCount: 0, + taskDefName: "inline", + scheduledTime: 1713413856975, + startTime: 1713413856975, + endTime: 1713413857004, + updateTime: 1713413856975, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "967c418b-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 1, + }, + workflowTask: { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + loopOver: [ + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref__1", + retryCount: 0, + seq: 3, + pollCount: 0, + taskDefName: "inline", + scheduledTime: 1713413856975, + startTime: 1713413856975, + endTime: 1713413857004, + updateTime: 1713413856975, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "967c418b-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 1, + }, + workflowTask: { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref__2", + retryCount: 0, + seq: 7, + pollCount: 0, + taskDefName: "inline", + scheduledTime: 1713413857247, + startTime: 1713413857247, + endTime: 1713413857262, + updateTime: 1713413857247, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "96a5e99f-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 2, + }, + workflowTask: { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 2, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref__3", + retryCount: 0, + seq: 11, + pollCount: 0, + taskDefName: "inline", + scheduledTime: 1713413857465, + startTime: 1713413857465, + endTime: 1713413857480, + updateTime: 1713413857465, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "96c70633-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 3, + }, + workflowTask: { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 3, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref__4", + retryCount: 0, + seq: 15, + pollCount: 0, + taskDefName: "inline", + scheduledTime: 1713413857687, + startTime: 1713413857687, + endTime: 1713413857702, + updateTime: 1713413857687, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "96e90d27-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 1, + }, + workflowTask: { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 4, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref__5", + retryCount: 0, + seq: 19, + pollCount: 0, + taskDefName: "inline", + scheduledTime: 1713413857930, + startTime: 1713413857930, + endTime: 1713413857945, + updateTime: 1713413857930, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "970e215b-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 1, + }, + workflowTask: { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 5, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref__6", + retryCount: 0, + seq: 23, + pollCount: 0, + taskDefName: "inline", + scheduledTime: 1713413858154, + startTime: 1713413858154, + endTime: 1713413858196, + updateTime: 1713413858154, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97304f5f-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 1, + }, + workflowTask: { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 6, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref__7", + retryCount: 0, + seq: 27, + pollCount: 0, + taskDefName: "inline", + scheduledTime: 1713413858383, + startTime: 1713413858383, + endTime: 1713413858402, + updateTime: 1713413858383, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "975319a3-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 2, + }, + workflowTask: { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 7, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref__8", + retryCount: 0, + seq: 31, + pollCount: 0, + taskDefName: "inline", + scheduledTime: 1713413858598, + startTime: 1713413858598, + endTime: 1713413858615, + updateTime: 1713413858598, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9773e817-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 3, + }, + workflowTask: { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 8, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref__9", + retryCount: 0, + seq: 35, + pollCount: 0, + taskDefName: "inline", + scheduledTime: 1713413858836, + startTime: 1713413858836, + endTime: 1713413858852, + updateTime: 1713413858836, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "979811eb-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 1, + }, + workflowTask: { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 9, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref__10", + retryCount: 0, + seq: 39, + pollCount: 0, + taskDefName: "inline", + scheduledTime: 1713413859056, + startTime: 1713413859056, + endTime: 1713413859075, + updateTime: 1713413859056, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97b9cabf-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 2, + }, + workflowTask: { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + ], + }, + switch_ref: { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 1, + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref__1", + retryCount: 0, + seq: 4, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413857015, + startTime: 1713413857015, + endTime: 1713413857015, + updateTime: 1713413857015, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "968171ac-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + loopOver: [ + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 1, + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref__1", + retryCount: 0, + seq: 4, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413857015, + startTime: 1713413857015, + endTime: 1713413857015, + updateTime: 1713413857015, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "968171ac-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 2, + _createdBy: "najeeb.thangal@orkes.io", + case: "2", + }, + referenceTaskName: "switch_ref__2", + retryCount: 0, + seq: 8, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413857270, + startTime: 1713413857270, + endTime: 1713413857270, + updateTime: 1713413857270, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "96a8f6e0-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["2"], + selectedCase: "2", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 2, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 3, + _createdBy: "najeeb.thangal@orkes.io", + case: "3", + }, + referenceTaskName: "switch_ref__3", + retryCount: 0, + seq: 12, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413857488, + startTime: 1713413857488, + endTime: 1713413857488, + updateTime: 1713413857488, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "96ca3a84-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["3"], + selectedCase: "3", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 3, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 1, + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref__4", + retryCount: 0, + seq: 16, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413857710, + startTime: 1713413857711, + endTime: 1713413857711, + updateTime: 1713413857711, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "96ebf358-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 4, + subworkflowChanged: false, + queueWaitTime: 1, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 1, + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref__5", + retryCount: 0, + seq: 20, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413857953, + startTime: 1713413857953, + endTime: 1713413857953, + updateTime: 1713413857953, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97112e9c-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 5, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 1, + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref__6", + retryCount: 0, + seq: 24, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413858204, + startTime: 1713413858204, + endTime: 1713413858204, + updateTime: 1713413858204, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97377b50-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 6, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 2, + _createdBy: "najeeb.thangal@orkes.io", + case: "2", + }, + referenceTaskName: "switch_ref__7", + retryCount: 0, + seq: 28, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413858412, + startTime: 1713413858412, + endTime: 1713413858412, + updateTime: 1713413858412, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97571144-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["2"], + selectedCase: "2", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 7, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 3, + _createdBy: "najeeb.thangal@orkes.io", + case: "3", + }, + referenceTaskName: "switch_ref__8", + retryCount: 0, + seq: 32, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413858625, + startTime: 1713413858625, + endTime: 1713413858625, + updateTime: 1713413858625, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97779198-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["3"], + selectedCase: "3", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 8, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 1, + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref__9", + retryCount: 0, + seq: 36, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413858864, + startTime: 1713413858864, + endTime: 1713413858864, + updateTime: 1713413858864, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "979c098c-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 9, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 2, + _createdBy: "najeeb.thangal@orkes.io", + case: "2", + }, + referenceTaskName: "switch_ref__10", + retryCount: 0, + seq: 40, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413859083, + startTime: 1713413859083, + endTime: 1713413859083, + updateTime: 1713413859083, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97bd7440-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["2"], + selectedCase: "2", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + ], + }, + http_ref_3: { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_3__1", + retryCount: 0, + seq: 5, + pollCount: 1, + taskDefName: "http_3", + scheduledTime: 1713413857007, + startTime: 1713413857108, + endTime: 1713413857122, + updateTime: 1713413857108, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "968171ad-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": ["max-age=15724800; includeSubDomains"], + Connection: ["keep-alive"], + "Content-Length": ["180"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 47, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "vxqmzdyagxmexnpbaiqd", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 101, + loopOverTask: true, + taskDefinition: null, + loopOver: [ + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_3__1", + retryCount: 0, + seq: 5, + pollCount: 1, + taskDefName: "http_3", + scheduledTime: 1713413857007, + startTime: 1713413857108, + endTime: 1713413857122, + updateTime: 1713413857108, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "968171ad-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["180"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 47, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "vxqmzdyagxmexnpbaiqd", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 101, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_3__4", + retryCount: 0, + seq: 17, + pollCount: 1, + taskDefName: "http_3", + scheduledTime: 1713413857705, + startTime: 1713413857770, + endTime: 1713413857805, + updateTime: 1713413857770, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "96ec1a69-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 3964, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "qsrrgcyapbgeuoceizaa", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 4, + subworkflowChanged: false, + queueWaitTime: 65, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_3__5", + retryCount: 0, + seq: 21, + pollCount: 1, + taskDefName: "http_3", + scheduledTime: 1713413857948, + startTime: 1713413858015, + endTime: 1713413858027, + updateTime: 1713413858015, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97112e9d-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 698, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "aobgjizxlwgiwdasfclh", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 5, + subworkflowChanged: false, + queueWaitTime: 67, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_3__6", + retryCount: 0, + seq: 25, + pollCount: 1, + taskDefName: "http_3", + scheduledTime: 1713413858199, + startTime: 1713413858238, + endTime: 1713413858249, + updateTime: 1713413858238, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97377b51-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2489, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "vbksjctcduvxcucqtykv", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 6, + subworkflowChanged: false, + queueWaitTime: 39, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_3__9", + retryCount: 0, + seq: 37, + pollCount: 1, + taskDefName: "http_3", + scheduledTime: 1713413858858, + startTime: 1713413858917, + endTime: 1713413858928, + updateTime: 1713413858917, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "979c098d-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 1122, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "qixyannkpllogxldfyqi", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 9, + subworkflowChanged: false, + queueWaitTime: 59, + loopOverTask: true, + taskDefinition: null, + }, + ], + }, + http_ref_10: { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_10__1", + retryCount: 0, + seq: 6, + pollCount: 1, + taskDefName: "http_10", + scheduledTime: 1713413857128, + startTime: 1713413857218, + endTime: 1713413857230, + updateTime: 1713413857218, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9693e83e-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": ["max-age=15724800; includeSubDomains"], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7523, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "rzlozabxrltfkexdijot", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 90, + loopOverTask: true, + taskDefinition: null, + loopOver: [ + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_10__1", + retryCount: 0, + seq: 6, + pollCount: 1, + taskDefName: "http_10", + scheduledTime: 1713413857128, + startTime: 1713413857218, + endTime: 1713413857230, + updateTime: 1713413857218, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9693e83e-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7523, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "rzlozabxrltfkexdijot", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 90, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_10__2", + retryCount: 0, + seq: 10, + pollCount: 1, + taskDefName: "http_10", + scheduledTime: 1713413857348, + startTime: 1713413857439, + endTime: 1713413857451, + updateTime: 1713413857439, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "96b5a112-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9295, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "bgnycyjkhkdmiqsnjpcm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 2, + subworkflowChanged: false, + queueWaitTime: 91, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_10__3", + retryCount: 0, + seq: 14, + pollCount: 1, + taskDefName: "http_10", + scheduledTime: 1713413857567, + startTime: 1713413857659, + endTime: 1713413857671, + updateTime: 1713413857659, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "96d70bc6-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 1008, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "drzushsnuxfkfbxjqvfh", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 3, + subworkflowChanged: false, + queueWaitTime: 92, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_10__4", + retryCount: 0, + seq: 18, + pollCount: 1, + taskDefName: "http_10", + scheduledTime: 1713413857810, + startTime: 1713413857904, + endTime: 1713413857915, + updateTime: 1713413857904, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "96fc1ffa-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 6761, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "taupmlrpyhpzpsulaxqo", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 4, + subworkflowChanged: false, + queueWaitTime: 94, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_10__5", + retryCount: 0, + seq: 22, + pollCount: 1, + taskDefName: "http_10", + scheduledTime: 1713413858034, + startTime: 1713413858126, + endTime: 1713413858139, + updateTime: 1713413858126, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "971e4dfe-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 6970, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "igwwnykfobqdnkxmafab", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 5, + subworkflowChanged: false, + queueWaitTime: 92, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_10__6", + retryCount: 0, + seq: 26, + pollCount: 1, + taskDefName: "http_10", + scheduledTime: 1713413858256, + startTime: 1713413858349, + endTime: 1713413858361, + updateTime: 1713413858349, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97402de2-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8930, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "purhjvrqkogxpcyjifxv", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 6, + subworkflowChanged: false, + queueWaitTime: 93, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_10__7", + retryCount: 0, + seq: 30, + pollCount: 1, + taskDefName: "http_10", + scheduledTime: 1713413858478, + startTime: 1713413858571, + endTime: 1713413858583, + updateTime: 1713413858571, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97620dc6-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5965, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "cozkeartchukblaomchw", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 7, + subworkflowChanged: false, + queueWaitTime: 93, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_10__8", + retryCount: 0, + seq: 34, + pollCount: 1, + taskDefName: "http_10", + scheduledTime: 1713413858712, + startTime: 1713413858806, + endTime: 1713413858818, + updateTime: 1713413858806, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9785c26a-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9726, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "qmkipozchmypxunkkkzd", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 8, + subworkflowChanged: false, + queueWaitTime: 94, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_10__9", + retryCount: 0, + seq: 38, + pollCount: 1, + taskDefName: "http_10", + scheduledTime: 1713413858936, + startTime: 1713413859029, + endTime: 1713413859040, + updateTime: 1713413859029, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97a7c95e-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2780, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "jklxzsbanazwjffbarxi", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 9, + subworkflowChanged: false, + queueWaitTime: 93, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_10__10", + retryCount: 0, + seq: 42, + pollCount: 1, + taskDefName: "http_10", + scheduledTime: 1713413859158, + startTime: 1713413859250, + endTime: 1713413859260, + updateTime: 1713413859250, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97c9d052-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 944, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "piubltpqkibleiqpmeno", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 92, + loopOverTask: true, + taskDefinition: null, + }, + ], + }, + http_ref_4: { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_4", + retryCount: 0, + seq: 43, + pollCount: 1, + taskDefName: "http_4", + scheduledTime: 1713413859277, + startTime: 1713413859361, + endTime: 1713413859373, + updateTime: 1713413859361, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97dbf8c3-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": ["max-age=15724800; includeSubDomains"], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5081, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "yfxgvcpjtxnjlftbtcpr", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_4", + taskReferenceName: "http_ref_4", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + queueWaitTime: 84, + loopOverTask: false, + taskDefinition: null, + loopOver: [ + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_4", + retryCount: 0, + seq: 43, + pollCount: 1, + taskDefName: "http_4", + scheduledTime: 1713413859277, + startTime: 1713413859361, + endTime: 1713413859373, + updateTime: 1713413859361, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97dbf8c3-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5081, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "yfxgvcpjtxnjlftbtcpr", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_4", + taskReferenceName: "http_ref_4", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + queueWaitTime: 84, + loopOverTask: false, + taskDefinition: null, + }, + ], + }, + do_while_ref_1: { + taskType: "DO_WHILE", + status: "COMPLETED", + inputData: { + number: 10, + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "do_while_ref_1", + retryCount: 0, + seq: 44, + pollCount: 0, + taskDefName: "do_while_1", + scheduledTime: 1713413859382, + startTime: 1713413859382, + endTime: 1713413862160, + updateTime: 1713413861832, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97eb8924-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + 1: { + switch_ref_1: { + evaluationResult: ["2"], + selectedCase: "2", + }, + inline_ref_1: { + result: 2, + }, + http_ref_7: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9943, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "tjxztrsclyzabbqspjmv", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9428, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "lwfynuupwbgkcwgqemcs", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 2: { + switch_ref_1: { + evaluationResult: ["3"], + selectedCase: "3", + }, + inline_ref_1: { + result: 3, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 3723, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "kjmdqmvozujqsxupnlot", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 154, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "obdruesgtvezidzuenhu", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 1892, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "heqrbsinaexemndlvmcp", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 3: { + switch_ref_1: { + evaluationResult: ["1"], + selectedCase: "1", + }, + inline_ref_1: { + result: 1, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7080, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "cuurineeimzdpveetkte", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9931, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "ktrbhjusvnttizwmopxg", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 3569, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "rbeetiujrcnurymrqpvl", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 4: { + switch_ref_1: { + evaluationResult: ["3"], + selectedCase: "3", + }, + inline_ref_1: { + result: 3, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8155, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "tniyursobyqwmcgnfxcj", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5254, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "fqlewtggkleyvghxoyqd", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7061, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "ixireilwipqxtseivuxm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 5: { + switch_ref_1: { + evaluationResult: ["1"], + selectedCase: "1", + }, + inline_ref_1: { + result: 1, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5878, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "bksldazxfxhyoyefvrxf", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2932, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "djokkjixbuwxlsstoaab", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9316, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "dytnpkdddlzmtzlwfpal", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 6: { + switch_ref_1: { + evaluationResult: ["4"], + selectedCase: "4", + }, + inline_ref_1: { + result: 4, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4089, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "khrlsfsjmhvsjgtshbfz", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 7: { + switch_ref_1: { + evaluationResult: ["4"], + selectedCase: "4", + }, + inline_ref_1: { + result: 4, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8433, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "nwltborbttslioxepwhe", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 8: { + switch_ref_1: { + evaluationResult: ["1"], + selectedCase: "1", + }, + inline_ref_1: { + result: 1, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 242, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "idkvujpflbawjvbtcqol", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5914, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "sccygcowawimarjtitnq", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2173, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "saxapbpahdmmtpavvjlm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 9: { + switch_ref_1: { + evaluationResult: ["1"], + selectedCase: "1", + }, + inline_ref_1: { + result: 1, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4359, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "lmbrmrnefvkwwwbmvolm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 395, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "myxiwwnhjbxhdwkfwmzm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2885, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "ykuzzhmdpvfcragcwxoh", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 10: { + switch_ref_1: { + evaluationResult: ["3"], + selectedCase: "3", + }, + inline_ref_1: { + result: 3, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:42 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4274, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "kwmoumdlucxuwgphkxaj", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5175, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "enhhdgrjmtgntufhymzm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:42 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4750, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "oeenqbhrzifhhetyzpcu", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + iteration: 10, + }, + workflowTask: { + name: "do_while_1", + taskReferenceName: "do_while_ref_1", + inputParameters: { + number: 10, + }, + type: "DO_WHILE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopCondition: + "(function () {\n if ($.do_while_ref_1['iteration'] < $.number) {\n return true;\n }\n return false;\n})();", + loopOver: [ + { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + evaluatorType: "graaljs", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + loopOver: [ + { + taskType: "DO_WHILE", + status: "COMPLETED", + inputData: { + number: 10, + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "do_while_ref_1", + retryCount: 0, + seq: 44, + pollCount: 0, + taskDefName: "do_while_1", + scheduledTime: 1713413859382, + startTime: 1713413859382, + endTime: 1713413862160, + updateTime: 1713413861832, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97eb8924-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + 1: { + switch_ref_1: { + evaluationResult: ["2"], + selectedCase: "2", + }, + inline_ref_1: { + result: 2, + }, + http_ref_7: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9943, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "tjxztrsclyzabbqspjmv", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9428, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "lwfynuupwbgkcwgqemcs", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 2: { + switch_ref_1: { + evaluationResult: ["3"], + selectedCase: "3", + }, + inline_ref_1: { + result: 3, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 3723, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "kjmdqmvozujqsxupnlot", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 154, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "obdruesgtvezidzuenhu", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 1892, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "heqrbsinaexemndlvmcp", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 3: { + switch_ref_1: { + evaluationResult: ["1"], + selectedCase: "1", + }, + inline_ref_1: { + result: 1, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7080, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "cuurineeimzdpveetkte", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9931, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "ktrbhjusvnttizwmopxg", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 3569, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "rbeetiujrcnurymrqpvl", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 4: { + switch_ref_1: { + evaluationResult: ["3"], + selectedCase: "3", + }, + inline_ref_1: { + result: 3, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8155, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "tniyursobyqwmcgnfxcj", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5254, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "fqlewtggkleyvghxoyqd", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7061, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "ixireilwipqxtseivuxm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 5: { + switch_ref_1: { + evaluationResult: ["1"], + selectedCase: "1", + }, + inline_ref_1: { + result: 1, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5878, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "bksldazxfxhyoyefvrxf", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2932, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "djokkjixbuwxlsstoaab", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9316, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "dytnpkdddlzmtzlwfpal", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 6: { + switch_ref_1: { + evaluationResult: ["4"], + selectedCase: "4", + }, + inline_ref_1: { + result: 4, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4089, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "khrlsfsjmhvsjgtshbfz", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 7: { + switch_ref_1: { + evaluationResult: ["4"], + selectedCase: "4", + }, + inline_ref_1: { + result: 4, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8433, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "nwltborbttslioxepwhe", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 8: { + switch_ref_1: { + evaluationResult: ["1"], + selectedCase: "1", + }, + inline_ref_1: { + result: 1, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 242, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "idkvujpflbawjvbtcqol", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5914, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "sccygcowawimarjtitnq", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2173, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "saxapbpahdmmtpavvjlm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 9: { + switch_ref_1: { + evaluationResult: ["1"], + selectedCase: "1", + }, + inline_ref_1: { + result: 1, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4359, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "lmbrmrnefvkwwwbmvolm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 395, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "myxiwwnhjbxhdwkfwmzm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2885, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "ykuzzhmdpvfcragcwxoh", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 10: { + switch_ref_1: { + evaluationResult: ["3"], + selectedCase: "3", + }, + inline_ref_1: { + result: 3, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:42 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4274, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "kwmoumdlucxuwgphkxaj", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5175, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "enhhdgrjmtgntufhymzm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:42 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4750, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "oeenqbhrzifhhetyzpcu", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + iteration: 10, + }, + workflowTask: { + name: "do_while_1", + taskReferenceName: "do_while_ref_1", + inputParameters: { + number: 10, + }, + type: "DO_WHILE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopCondition: + "(function () {\n if ($.do_while_ref_1['iteration'] < $.number) {\n return true;\n }\n return false;\n})();", + loopOver: [ + { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + evaluatorType: "graaljs", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + ], + }, + inline_ref_1: { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref_1__10", + retryCount: 0, + seq: 85, + pollCount: 0, + taskDefName: "inline_1", + scheduledTime: 1713413861829, + startTime: 1713413861829, + endTime: 1713413861842, + updateTime: 1713413861829, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9960760d-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 3, + }, + workflowTask: { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + loopOver: [ + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref_1__1", + retryCount: 0, + seq: 45, + pollCount: 0, + taskDefName: "inline_1", + scheduledTime: 1713413859386, + startTime: 1713413859386, + endTime: 1713413859402, + updateTime: 1713413859386, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97ec2565-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 2, + }, + workflowTask: { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref_1__2", + retryCount: 0, + seq: 49, + pollCount: 0, + taskDefName: "inline_1", + scheduledTime: 1713413859617, + startTime: 1713413859617, + endTime: 1713413859629, + updateTime: 1713413859617, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "980f64d9-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 3, + }, + workflowTask: { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 2, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref_1__3", + retryCount: 0, + seq: 54, + pollCount: 0, + taskDefName: "inline_1", + scheduledTime: 1713413859945, + startTime: 1713413859945, + endTime: 1713413859959, + updateTime: 1713413859945, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9841715e-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 1, + }, + workflowTask: { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 3, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref_1__4", + retryCount: 0, + seq: 59, + pollCount: 0, + taskDefName: "inline_1", + scheduledTime: 1713413860274, + startTime: 1713413860274, + endTime: 1713413860287, + updateTime: 1713413860274, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9873a4f3-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 3, + }, + workflowTask: { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 4, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref_1__5", + retryCount: 0, + seq: 64, + pollCount: 0, + taskDefName: "inline_1", + scheduledTime: 1713413860607, + startTime: 1713413860607, + endTime: 1713413860621, + updateTime: 1713413860607, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98a69bd8-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 1, + }, + workflowTask: { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 5, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref_1__6", + retryCount: 0, + seq: 69, + pollCount: 0, + taskDefName: "inline_1", + scheduledTime: 1713413860941, + startTime: 1713413860941, + endTime: 1713413860957, + updateTime: 1713413860941, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98d96bad-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 4, + }, + workflowTask: { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 6, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref_1__7", + retryCount: 0, + seq: 72, + pollCount: 0, + taskDefName: "inline_1", + scheduledTime: 1713413861052, + startTime: 1713413861052, + endTime: 1713413861070, + updateTime: 1713413861052, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98ea3490-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 4, + }, + workflowTask: { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 7, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref_1__8", + retryCount: 0, + seq: 75, + pollCount: 0, + taskDefName: "inline_1", + scheduledTime: 1713413861161, + startTime: 1713413861161, + endTime: 1713413861175, + updateTime: 1713413861161, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98fafd73-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 1, + }, + workflowTask: { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 8, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref_1__9", + retryCount: 0, + seq: 80, + pollCount: 0, + taskDefName: "inline_1", + scheduledTime: 1713413861496, + startTime: 1713413861497, + endTime: 1713413861510, + updateTime: 1713413861497, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "992e4278-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 1, + }, + workflowTask: { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 9, + subworkflowChanged: false, + queueWaitTime: 1, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref_1__10", + retryCount: 0, + seq: 85, + pollCount: 0, + taskDefName: "inline_1", + scheduledTime: 1713413861829, + startTime: 1713413861829, + endTime: 1713413861842, + updateTime: 1713413861829, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9960760d-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 3, + }, + workflowTask: { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + ], + }, + switch_ref_1: { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 3, + _createdBy: "najeeb.thangal@orkes.io", + case: "3", + }, + referenceTaskName: "switch_ref_1__10", + retryCount: 0, + seq: 86, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413861851, + startTime: 1713413861851, + endTime: 1713413861851, + updateTime: 1713413861851, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9963d16e-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["3"], + selectedCase: "3", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + loopOver: [ + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 2, + _createdBy: "najeeb.thangal@orkes.io", + case: "2", + }, + referenceTaskName: "switch_ref_1__1", + retryCount: 0, + seq: 46, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413859413, + startTime: 1713413859413, + endTime: 1713413859413, + updateTime: 1713413859413, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97ef80c6-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["2"], + selectedCase: "2", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 3, + _createdBy: "najeeb.thangal@orkes.io", + case: "3", + }, + referenceTaskName: "switch_ref_1__2", + retryCount: 0, + seq: 50, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413859637, + startTime: 1713413859637, + endTime: 1713413859637, + updateTime: 1713413859637, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9811fcea-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["3"], + selectedCase: "3", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 2, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 1, + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref_1__3", + retryCount: 0, + seq: 55, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413859968, + startTime: 1713413859968, + endTime: 1713413859968, + updateTime: 1713413859968, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98447e9f-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 3, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 3, + _createdBy: "najeeb.thangal@orkes.io", + case: "3", + }, + referenceTaskName: "switch_ref_1__4", + retryCount: 0, + seq: 60, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413860295, + startTime: 1713413860295, + endTime: 1713413860295, + updateTime: 1713413860295, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98768b24-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["3"], + selectedCase: "3", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 4, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 1, + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref_1__5", + retryCount: 0, + seq: 65, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413860629, + startTime: 1713413860629, + endTime: 1713413860629, + updateTime: 1713413860629, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98a98209-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 5, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + switchCaseValue: 4, + _createdBy: "najeeb.thangal@orkes.io", + case: "4", + }, + referenceTaskName: "switch_ref_1__6", + retryCount: 0, + seq: 70, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413860963, + startTime: 1713413860963, + endTime: 1713413860963, + updateTime: 1713413860963, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98dcc70e-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["4"], + selectedCase: "4", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 6, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + switchCaseValue: 4, + _createdBy: "najeeb.thangal@orkes.io", + case: "4", + }, + referenceTaskName: "switch_ref_1__7", + retryCount: 0, + seq: 73, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413861076, + startTime: 1713413861076, + endTime: 1713413861076, + updateTime: 1713413861076, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98ee0521-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["4"], + selectedCase: "4", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 7, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 1, + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref_1__8", + retryCount: 0, + seq: 76, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413861183, + startTime: 1713413861183, + endTime: 1713413861183, + updateTime: 1713413861183, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98fe0ab4-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 8, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 1, + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref_1__9", + retryCount: 0, + seq: 81, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413861520, + startTime: 1713413861520, + endTime: 1713413861520, + updateTime: 1713413861520, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "993128a9-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 9, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 3, + _createdBy: "najeeb.thangal@orkes.io", + case: "3", + }, + referenceTaskName: "switch_ref_1__10", + retryCount: 0, + seq: 86, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413861851, + startTime: 1713413861851, + endTime: 1713413861851, + updateTime: 1713413861851, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9963d16e-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["3"], + selectedCase: "3", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + ], + }, + http_ref_7: { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_7__1", + retryCount: 0, + seq: 47, + pollCount: 1, + taskDefName: "http_7", + scheduledTime: 1713413859406, + startTime: 1713413859472, + endTime: 1713413859483, + updateTime: 1713413859472, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97efa7d7-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": ["max-age=15724800; includeSubDomains"], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9943, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "tjxztrsclyzabbqspjmv", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 66, + loopOverTask: true, + taskDefinition: null, + loopOver: [ + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_7__1", + retryCount: 0, + seq: 47, + pollCount: 1, + taskDefName: "http_7", + scheduledTime: 1713413859406, + startTime: 1713413859472, + endTime: 1713413859483, + updateTime: 1713413859472, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97efa7d7-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9943, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "tjxztrsclyzabbqspjmv", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 66, + loopOverTask: true, + taskDefinition: null, + }, + ], + }, + http_ref_8: { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_8__10", + retryCount: 0, + seq: 89, + pollCount: 1, + taskDefName: "http_8", + scheduledTime: 1713413862045, + startTime: 1713413862132, + endTime: 1713413862145, + updateTime: 1713413862132, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "998255f1-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": ["max-age=15724800; includeSubDomains"], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:42 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4274, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "kwmoumdlucxuwgphkxaj", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 87, + loopOverTask: true, + taskDefinition: null, + loopOver: [ + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_8__1", + retryCount: 0, + seq: 48, + pollCount: 1, + taskDefName: "http_8", + scheduledTime: 1713413859490, + startTime: 1713413859583, + endTime: 1713413859594, + updateTime: 1713413859583, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97fc7918-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9428, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "lwfynuupwbgkcwgqemcs", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 93, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_8__2", + retryCount: 0, + seq: 53, + pollCount: 1, + taskDefName: "http_8", + scheduledTime: 1713413859823, + startTime: 1713413859916, + endTime: 1713413859927, + updateTime: 1713413859916, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "982f48ed-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 3723, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "kjmdqmvozujqsxupnlot", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 2, + subworkflowChanged: false, + queueWaitTime: 93, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_8__3", + retryCount: 0, + seq: 58, + pollCount: 1, + taskDefName: "http_8", + scheduledTime: 1713413860156, + startTime: 1713413860248, + endTime: 1713413860259, + updateTime: 1713413860248, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "986218c2-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7080, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "cuurineeimzdpveetkte", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 3, + subworkflowChanged: false, + queueWaitTime: 92, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_8__4", + retryCount: 0, + seq: 63, + pollCount: 1, + taskDefName: "http_8", + scheduledTime: 1713413860487, + startTime: 1713413860580, + endTime: 1713413860592, + updateTime: 1713413860580, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98949a77-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8155, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "tniyursobyqwmcgnfxcj", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 4, + subworkflowChanged: false, + queueWaitTime: 93, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_8__5", + retryCount: 0, + seq: 68, + pollCount: 1, + taskDefName: "http_8", + scheduledTime: 1713413860819, + startTime: 1713413860914, + endTime: 1713413860925, + updateTime: 1713413860914, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98c7433c-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5878, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "bksldazxfxhyoyefvrxf", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 5, + subworkflowChanged: false, + queueWaitTime: 95, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_8__6", + retryCount: 0, + seq: 71, + pollCount: 1, + taskDefName: "http_8", + scheduledTime: 1713413860970, + startTime: 1713413861025, + endTime: 1713413861036, + updateTime: 1713413861025, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98de269f-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4089, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "khrlsfsjmhvsjgtshbfz", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 6, + subworkflowChanged: false, + queueWaitTime: 55, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_8__7", + retryCount: 0, + seq: 74, + pollCount: 1, + taskDefName: "http_8", + scheduledTime: 1713413861082, + startTime: 1713413861136, + endTime: 1713413861147, + updateTime: 1713413861136, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98ef64b2-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8433, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "nwltborbttslioxepwhe", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 7, + subworkflowChanged: false, + queueWaitTime: 54, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_8__8", + retryCount: 0, + seq: 79, + pollCount: 1, + taskDefName: "http_8", + scheduledTime: 1713413861375, + startTime: 1713413861467, + endTime: 1713413861478, + updateTime: 1713413861467, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "991c1a07-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 242, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "idkvujpflbawjvbtcqol", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 8, + subworkflowChanged: false, + queueWaitTime: 92, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_8__9", + retryCount: 0, + seq: 84, + pollCount: 1, + taskDefName: "http_8", + scheduledTime: 1713413861704, + startTime: 1713413861798, + endTime: 1713413861810, + updateTime: 1713413861798, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "994e4d9c-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4359, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "lmbrmrnefvkwwwbmvolm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 9, + subworkflowChanged: false, + queueWaitTime: 94, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_8__10", + retryCount: 0, + seq: 89, + pollCount: 1, + taskDefName: "http_8", + scheduledTime: 1713413862045, + startTime: 1713413862132, + endTime: 1713413862145, + updateTime: 1713413862132, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "998255f1-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:42 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4274, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "kwmoumdlucxuwgphkxaj", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 87, + loopOverTask: true, + taskDefinition: null, + }, + ], + }, + http_ref_5: { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_5__10", + retryCount: 0, + seq: 87, + pollCount: 1, + taskDefName: "http_5", + scheduledTime: 1713413861845, + startTime: 1713413861911, + endTime: 1713413861920, + updateTime: 1713413861911, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9963d16f-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": ["max-age=15724800; includeSubDomains"], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5175, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "enhhdgrjmtgntufhymzm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 66, + loopOverTask: true, + taskDefinition: null, + loopOver: [ + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_5__2", + retryCount: 0, + seq: 51, + pollCount: 1, + taskDefName: "http_5", + scheduledTime: 1713413859632, + startTime: 1713413859695, + endTime: 1713413859706, + updateTime: 1713413859695, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "981223fb-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 154, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "obdruesgtvezidzuenhu", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 2, + subworkflowChanged: false, + queueWaitTime: 63, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_5__3", + retryCount: 0, + seq: 56, + pollCount: 1, + taskDefName: "http_5", + scheduledTime: 1713413859962, + startTime: 1713413860027, + endTime: 1713413860038, + updateTime: 1713413860027, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98447ea0-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9931, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "ktrbhjusvnttizwmopxg", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 3, + subworkflowChanged: false, + queueWaitTime: 65, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_5__4", + retryCount: 0, + seq: 61, + pollCount: 1, + taskDefName: "http_5", + scheduledTime: 1713413860290, + startTime: 1713413860358, + endTime: 1713413860370, + updateTime: 1713413860358, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98768b25-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5254, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "fqlewtggkleyvghxoyqd", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 4, + subworkflowChanged: false, + queueWaitTime: 68, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_5__5", + retryCount: 0, + seq: 66, + pollCount: 1, + taskDefName: "http_5", + scheduledTime: 1713413860624, + startTime: 1713413860692, + endTime: 1713413860703, + updateTime: 1713413860692, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98a9820a-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2932, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "djokkjixbuwxlsstoaab", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 5, + subworkflowChanged: false, + queueWaitTime: 68, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_5__8", + retryCount: 0, + seq: 77, + pollCount: 1, + taskDefName: "http_5", + scheduledTime: 1713413861178, + startTime: 1713413861247, + endTime: 1713413861257, + updateTime: 1713413861247, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98fe0ab5-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5914, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "sccygcowawimarjtitnq", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 8, + subworkflowChanged: false, + queueWaitTime: 69, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_5__9", + retryCount: 0, + seq: 82, + pollCount: 1, + taskDefName: "http_5", + scheduledTime: 1713413861514, + startTime: 1713413861577, + endTime: 1713413861588, + updateTime: 1713413861577, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "99314fba-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 395, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "myxiwwnhjbxhdwkfwmzm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 9, + subworkflowChanged: false, + queueWaitTime: 63, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_5__10", + retryCount: 0, + seq: 87, + pollCount: 1, + taskDefName: "http_5", + scheduledTime: 1713413861845, + startTime: 1713413861911, + endTime: 1713413861920, + updateTime: 1713413861911, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9963d16f-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5175, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "enhhdgrjmtgntufhymzm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 66, + loopOverTask: true, + taskDefinition: null, + }, + ], + }, + http_ref_6: { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_6__10", + retryCount: 0, + seq: 88, + pollCount: 1, + taskDefName: "http_6", + scheduledTime: 1713413861927, + startTime: 1713413862022, + endTime: 1713413862033, + updateTime: 1713413862022, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "99705490-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": ["max-age=15724800; includeSubDomains"], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:42 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4750, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "oeenqbhrzifhhetyzpcu", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 95, + loopOverTask: true, + taskDefinition: null, + loopOver: [ + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_6__2", + retryCount: 0, + seq: 52, + pollCount: 1, + taskDefName: "http_6", + scheduledTime: 1713413859717, + startTime: 1713413859806, + endTime: 1713413859816, + updateTime: 1713413859806, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "981f1c4c-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 1892, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "heqrbsinaexemndlvmcp", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 2, + subworkflowChanged: false, + queueWaitTime: 89, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_6__3", + retryCount: 0, + seq: 57, + pollCount: 1, + taskDefName: "http_6", + scheduledTime: 1713413860044, + startTime: 1713413860138, + endTime: 1713413860150, + updateTime: 1713413860138, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "985101c1-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 3569, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "rbeetiujrcnurymrqpvl", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 3, + subworkflowChanged: false, + queueWaitTime: 94, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_6__4", + retryCount: 0, + seq: 62, + pollCount: 1, + taskDefName: "http_6", + scheduledTime: 1713413860376, + startTime: 1713413860469, + endTime: 1713413860480, + updateTime: 1713413860469, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9883aa86-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7061, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "ixireilwipqxtseivuxm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 4, + subworkflowChanged: false, + queueWaitTime: 93, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_6__5", + retryCount: 0, + seq: 67, + pollCount: 1, + taskDefName: "http_6", + scheduledTime: 1713413860709, + startTime: 1713413860803, + endTime: 1713413860813, + updateTime: 1713413860803, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98b67a5b-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9316, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "dytnpkdddlzmtzlwfpal", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 5, + subworkflowChanged: false, + queueWaitTime: 94, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_6__8", + retryCount: 0, + seq: 78, + pollCount: 1, + taskDefName: "http_6", + scheduledTime: 1713413861263, + startTime: 1713413861357, + endTime: 1713413861368, + updateTime: 1713413861357, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "990b0306-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2173, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "saxapbpahdmmtpavvjlm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 8, + subworkflowChanged: false, + queueWaitTime: 94, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_6__9", + retryCount: 0, + seq: 83, + pollCount: 1, + taskDefName: "http_6", + scheduledTime: 1713413861594, + startTime: 1713413861688, + endTime: 1713413861698, + updateTime: 1713413861688, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "993d84bb-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2885, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "ykuzzhmdpvfcragcwxoh", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 9, + subworkflowChanged: false, + queueWaitTime: 94, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_6__10", + retryCount: 0, + seq: 88, + pollCount: 1, + taskDefName: "http_6", + scheduledTime: 1713413861927, + startTime: 1713413862022, + endTime: 1713413862033, + updateTime: 1713413862022, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "99705490-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:42 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4750, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "oeenqbhrzifhhetyzpcu", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 95, + loopOverTask: true, + taskDefinition: null, + }, + ], + }, + http_ref_9: { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_9", + retryCount: 0, + seq: 90, + pollCount: 1, + taskDefName: "http_9", + scheduledTime: 1713413862163, + startTime: 1713413862244, + endTime: 1713413862258, + updateTime: 1713413862244, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "99945752-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": ["max-age=15724800; includeSubDomains"], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:42 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2042, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "xwadanvnxdolxjqnchsa", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_9", + taskReferenceName: "http_ref_9", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + queueWaitTime: 81, + loopOverTask: false, + taskDefinition: null, + loopOver: [ + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_9", + retryCount: 0, + seq: 90, + pollCount: 1, + taskDefName: "http_9", + scheduledTime: 1713413862163, + startTime: 1713413862244, + endTime: 1713413862258, + updateTime: 1713413862244, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "99945752-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:42 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2042, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "xwadanvnxdolxjqnchsa", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_9", + taskReferenceName: "http_ref_9", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + queueWaitTime: 81, + loopOverTask: false, + taskDefinition: null, + }, + ], + }, +}; + +export const doWhileSelectionForStatusMapResultMultiDowhile = { + http_ref: { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref", + retryCount: 0, + seq: 1, + pollCount: 1, + taskDefName: "http", + scheduledTime: 1713413856786, + startTime: 1713413856891, + endTime: 1713413856963, + updateTime: 1713413856891, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "965fb8d9-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": ["max-age=15724800; includeSubDomains"], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:36 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8678, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "mmxpwylvytawptmkxykq", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http", + taskReferenceName: "http_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + queueWaitTime: 105, + loopOverTask: false, + taskDefinition: null, + loopOver: [ + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref", + retryCount: 0, + seq: 1, + pollCount: 1, + taskDefName: "http", + scheduledTime: 1713413856786, + startTime: 1713413856891, + endTime: 1713413856963, + updateTime: 1713413856891, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "965fb8d9-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:36 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8678, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "mmxpwylvytawptmkxykq", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http", + taskReferenceName: "http_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + queueWaitTime: 105, + loopOverTask: false, + taskDefinition: null, + }, + ], + }, + do_while_ref: { + taskType: "DO_WHILE", + status: "COMPLETED", + inputData: { + number: 10, + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "do_while_ref", + retryCount: 0, + seq: 2, + pollCount: 0, + taskDefName: "do_while", + scheduledTime: 1713413856971, + startTime: 1713413856971, + endTime: 1713413859275, + updateTime: 1713413859060, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "967bcc5a-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + 1: { + inline_ref: { + result: 1, + }, + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_ref_3: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["180"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 47, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "vxqmzdyagxmexnpbaiqd", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7523, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "rzlozabxrltfkexdijot", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 2: { + http_ref_2: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 633, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "pnnnyucqifmchdazstau", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + inline_ref: { + result: 2, + }, + switch_ref: { + evaluationResult: ["2"], + selectedCase: "2", + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9295, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "bgnycyjkhkdmiqsnjpcm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 3: { + http_ref_1: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5570, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "sxopfwobgmlbyhectkue", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + inline_ref: { + result: 3, + }, + switch_ref: { + evaluationResult: ["3"], + selectedCase: "3", + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 1008, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "drzushsnuxfkfbxjqvfh", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 4: { + inline_ref: { + result: 1, + }, + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_ref_3: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 3964, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "qsrrgcyapbgeuoceizaa", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 6761, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "taupmlrpyhpzpsulaxqo", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 5: { + inline_ref: { + result: 1, + }, + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_ref_3: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 698, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "aobgjizxlwgiwdasfclh", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 6970, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "igwwnykfobqdnkxmafab", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 6: { + inline_ref: { + result: 1, + }, + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_ref_3: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2489, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "vbksjctcduvxcucqtykv", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8930, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "purhjvrqkogxpcyjifxv", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 7: { + http_ref_2: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 970, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "abxkbgqcvgvqnxjcishg", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + inline_ref: { + result: 2, + }, + switch_ref: { + evaluationResult: ["2"], + selectedCase: "2", + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5965, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "cozkeartchukblaomchw", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 8: { + http_ref_1: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5144, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "ffeeyciploflvzcqtrsc", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + inline_ref: { + result: 3, + }, + switch_ref: { + evaluationResult: ["3"], + selectedCase: "3", + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9726, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "qmkipozchmypxunkkkzd", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 9: { + inline_ref: { + result: 1, + }, + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_ref_3: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 1122, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "qixyannkpllogxldfyqi", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2780, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "jklxzsbanazwjffbarxi", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 10: { + http_ref_2: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7706, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "wadmrkumznvlcrfbfpoz", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + inline_ref: { + result: 2, + }, + switch_ref: { + evaluationResult: ["2"], + selectedCase: "2", + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 944, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "piubltpqkibleiqpmeno", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + iteration: 10, + }, + workflowTask: { + name: "do_while", + taskReferenceName: "do_while_ref", + inputParameters: { + number: 10, + }, + type: "DO_WHILE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopCondition: + "(function () {\n if ($.do_while_ref['iteration'] < $.number) {\n return true;\n }\n return false;\n})();", + loopOver: [ + { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + evaluatorType: "graaljs", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + loopOver: [ + { + taskType: "DO_WHILE", + status: "COMPLETED", + inputData: { + number: 10, + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "do_while_ref", + retryCount: 0, + seq: 2, + pollCount: 0, + taskDefName: "do_while", + scheduledTime: 1713413856971, + startTime: 1713413856971, + endTime: 1713413859275, + updateTime: 1713413859060, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "967bcc5a-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + 1: { + inline_ref: { + result: 1, + }, + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_ref_3: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["180"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 47, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "vxqmzdyagxmexnpbaiqd", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7523, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "rzlozabxrltfkexdijot", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 2: { + http_ref_2: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 633, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "pnnnyucqifmchdazstau", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + inline_ref: { + result: 2, + }, + switch_ref: { + evaluationResult: ["2"], + selectedCase: "2", + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9295, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "bgnycyjkhkdmiqsnjpcm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 3: { + http_ref_1: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5570, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "sxopfwobgmlbyhectkue", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + inline_ref: { + result: 3, + }, + switch_ref: { + evaluationResult: ["3"], + selectedCase: "3", + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 1008, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "drzushsnuxfkfbxjqvfh", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 4: { + inline_ref: { + result: 1, + }, + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_ref_3: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 3964, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "qsrrgcyapbgeuoceizaa", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 6761, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "taupmlrpyhpzpsulaxqo", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 5: { + inline_ref: { + result: 1, + }, + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_ref_3: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 698, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "aobgjizxlwgiwdasfclh", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 6970, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "igwwnykfobqdnkxmafab", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 6: { + inline_ref: { + result: 1, + }, + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_ref_3: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2489, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "vbksjctcduvxcucqtykv", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8930, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "purhjvrqkogxpcyjifxv", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 7: { + http_ref_2: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 970, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "abxkbgqcvgvqnxjcishg", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + inline_ref: { + result: 2, + }, + switch_ref: { + evaluationResult: ["2"], + selectedCase: "2", + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5965, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "cozkeartchukblaomchw", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 8: { + http_ref_1: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5144, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "ffeeyciploflvzcqtrsc", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + inline_ref: { + result: 3, + }, + switch_ref: { + evaluationResult: ["3"], + selectedCase: "3", + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9726, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "qmkipozchmypxunkkkzd", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 9: { + inline_ref: { + result: 1, + }, + switch_ref: { + evaluationResult: ["1"], + selectedCase: "1", + }, + http_ref_3: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 1122, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "qixyannkpllogxldfyqi", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2780, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "jklxzsbanazwjffbarxi", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 10: { + http_ref_2: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7706, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "wadmrkumznvlcrfbfpoz", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + inline_ref: { + result: 2, + }, + switch_ref: { + evaluationResult: ["2"], + selectedCase: "2", + }, + http_ref_10: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 944, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "piubltpqkibleiqpmeno", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + iteration: 10, + }, + workflowTask: { + name: "do_while", + taskReferenceName: "do_while_ref", + inputParameters: { + number: 10, + }, + type: "DO_WHILE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopCondition: + "(function () {\n if ($.do_while_ref['iteration'] < $.number) {\n return true;\n }\n return false;\n})();", + loopOver: [ + { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + evaluatorType: "graaljs", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + ], + }, + inline_ref: { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref__1", + retryCount: 0, + seq: 3, + pollCount: 0, + taskDefName: "inline", + scheduledTime: 1713413856975, + startTime: 1713413856975, + endTime: 1713413857004, + updateTime: 1713413856975, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "967c418b-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 1, + }, + workflowTask: { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + loopOver: [ + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref__1", + retryCount: 0, + seq: 3, + pollCount: 0, + taskDefName: "inline", + scheduledTime: 1713413856975, + startTime: 1713413856975, + endTime: 1713413857004, + updateTime: 1713413856975, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "967c418b-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 1, + }, + workflowTask: { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref__2", + retryCount: 0, + seq: 7, + pollCount: 0, + taskDefName: "inline", + scheduledTime: 1713413857247, + startTime: 1713413857247, + endTime: 1713413857262, + updateTime: 1713413857247, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "96a5e99f-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 2, + }, + workflowTask: { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 2, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref__3", + retryCount: 0, + seq: 11, + pollCount: 0, + taskDefName: "inline", + scheduledTime: 1713413857465, + startTime: 1713413857465, + endTime: 1713413857480, + updateTime: 1713413857465, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "96c70633-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 3, + }, + workflowTask: { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 3, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref__4", + retryCount: 0, + seq: 15, + pollCount: 0, + taskDefName: "inline", + scheduledTime: 1713413857687, + startTime: 1713413857687, + endTime: 1713413857702, + updateTime: 1713413857687, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "96e90d27-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 1, + }, + workflowTask: { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 4, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref__5", + retryCount: 0, + seq: 19, + pollCount: 0, + taskDefName: "inline", + scheduledTime: 1713413857930, + startTime: 1713413857930, + endTime: 1713413857945, + updateTime: 1713413857930, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "970e215b-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 1, + }, + workflowTask: { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 5, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref__6", + retryCount: 0, + seq: 23, + pollCount: 0, + taskDefName: "inline", + scheduledTime: 1713413858154, + startTime: 1713413858154, + endTime: 1713413858196, + updateTime: 1713413858154, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97304f5f-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 1, + }, + workflowTask: { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 6, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref__7", + retryCount: 0, + seq: 27, + pollCount: 0, + taskDefName: "inline", + scheduledTime: 1713413858383, + startTime: 1713413858383, + endTime: 1713413858402, + updateTime: 1713413858383, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "975319a3-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 2, + }, + workflowTask: { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 7, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref__8", + retryCount: 0, + seq: 31, + pollCount: 0, + taskDefName: "inline", + scheduledTime: 1713413858598, + startTime: 1713413858598, + endTime: 1713413858615, + updateTime: 1713413858598, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9773e817-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 3, + }, + workflowTask: { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 8, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref__9", + retryCount: 0, + seq: 35, + pollCount: 0, + taskDefName: "inline", + scheduledTime: 1713413858836, + startTime: 1713413858836, + endTime: 1713413858852, + updateTime: 1713413858836, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "979811eb-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 1, + }, + workflowTask: { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 9, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref__10", + retryCount: 0, + seq: 39, + pollCount: 0, + taskDefName: "inline", + scheduledTime: 1713413859056, + startTime: 1713413859056, + endTime: 1713413859075, + updateTime: 1713413859056, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97b9cabf-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 2, + }, + workflowTask: { + name: "inline", + taskReferenceName: "inline_ref", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() *3) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + ], + }, + switch_ref: { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 1, + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref__1", + retryCount: 0, + seq: 4, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413857015, + startTime: 1713413857015, + endTime: 1713413857015, + updateTime: 1713413857015, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "968171ac-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + loopOver: [ + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 1, + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref__1", + retryCount: 0, + seq: 4, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413857015, + startTime: 1713413857015, + endTime: 1713413857015, + updateTime: 1713413857015, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "968171ac-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 2, + _createdBy: "najeeb.thangal@orkes.io", + case: "2", + }, + referenceTaskName: "switch_ref__2", + retryCount: 0, + seq: 8, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413857270, + startTime: 1713413857270, + endTime: 1713413857270, + updateTime: 1713413857270, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "96a8f6e0-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["2"], + selectedCase: "2", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 2, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 3, + _createdBy: "najeeb.thangal@orkes.io", + case: "3", + }, + referenceTaskName: "switch_ref__3", + retryCount: 0, + seq: 12, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413857488, + startTime: 1713413857488, + endTime: 1713413857488, + updateTime: 1713413857488, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "96ca3a84-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["3"], + selectedCase: "3", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 3, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 1, + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref__4", + retryCount: 0, + seq: 16, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413857710, + startTime: 1713413857711, + endTime: 1713413857711, + updateTime: 1713413857711, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "96ebf358-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 4, + subworkflowChanged: false, + queueWaitTime: 1, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 1, + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref__5", + retryCount: 0, + seq: 20, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413857953, + startTime: 1713413857953, + endTime: 1713413857953, + updateTime: 1713413857953, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97112e9c-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 5, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 1, + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref__6", + retryCount: 0, + seq: 24, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413858204, + startTime: 1713413858204, + endTime: 1713413858204, + updateTime: 1713413858204, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97377b50-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 6, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 2, + _createdBy: "najeeb.thangal@orkes.io", + case: "2", + }, + referenceTaskName: "switch_ref__7", + retryCount: 0, + seq: 28, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413858412, + startTime: 1713413858412, + endTime: 1713413858412, + updateTime: 1713413858412, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97571144-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["2"], + selectedCase: "2", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 7, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 3, + _createdBy: "najeeb.thangal@orkes.io", + case: "3", + }, + referenceTaskName: "switch_ref__8", + retryCount: 0, + seq: 32, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413858625, + startTime: 1713413858625, + endTime: 1713413858625, + updateTime: 1713413858625, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97779198-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["3"], + selectedCase: "3", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 8, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 1, + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref__9", + retryCount: 0, + seq: 36, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413858864, + startTime: 1713413858864, + endTime: 1713413858864, + updateTime: 1713413858864, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "979c098c-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 9, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 2, + _createdBy: "najeeb.thangal@orkes.io", + case: "2", + }, + referenceTaskName: "switch_ref__10", + retryCount: 0, + seq: 40, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413859083, + startTime: 1713413859083, + endTime: 1713413859083, + updateTime: 1713413859083, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97bd7440-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["2"], + selectedCase: "2", + }, + workflowTask: { + name: "switch", + taskReferenceName: "switch_ref", + inputParameters: { + switchCaseValue: "${inline_ref.output.result}", + }, + type: "SWITCH", + decisionCases: { + 1: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 2: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + }, + defaultCase: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + ], + }, + http_ref_3: { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_3__1", + retryCount: 0, + seq: 5, + pollCount: 1, + taskDefName: "http_3", + scheduledTime: 1713413857007, + startTime: 1713413857108, + endTime: 1713413857122, + updateTime: 1713413857108, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "968171ad-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": ["max-age=15724800; includeSubDomains"], + Connection: ["keep-alive"], + "Content-Length": ["180"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 47, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "vxqmzdyagxmexnpbaiqd", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 101, + loopOverTask: true, + taskDefinition: null, + loopOver: [ + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_3__1", + retryCount: 0, + seq: 5, + pollCount: 1, + taskDefName: "http_3", + scheduledTime: 1713413857007, + startTime: 1713413857108, + endTime: 1713413857122, + updateTime: 1713413857108, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "968171ad-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["180"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 47, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "vxqmzdyagxmexnpbaiqd", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 101, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_3__4", + retryCount: 0, + seq: 17, + pollCount: 1, + taskDefName: "http_3", + scheduledTime: 1713413857705, + startTime: 1713413857770, + endTime: 1713413857805, + updateTime: 1713413857770, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "96ec1a69-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 3964, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "qsrrgcyapbgeuoceizaa", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 4, + subworkflowChanged: false, + queueWaitTime: 65, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_3__5", + retryCount: 0, + seq: 21, + pollCount: 1, + taskDefName: "http_3", + scheduledTime: 1713413857948, + startTime: 1713413858015, + endTime: 1713413858027, + updateTime: 1713413858015, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97112e9d-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 698, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "aobgjizxlwgiwdasfclh", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 5, + subworkflowChanged: false, + queueWaitTime: 67, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_3__6", + retryCount: 0, + seq: 25, + pollCount: 1, + taskDefName: "http_3", + scheduledTime: 1713413858199, + startTime: 1713413858238, + endTime: 1713413858249, + updateTime: 1713413858238, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97377b51-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2489, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "vbksjctcduvxcucqtykv", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 6, + subworkflowChanged: false, + queueWaitTime: 39, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_3__9", + retryCount: 0, + seq: 37, + pollCount: 1, + taskDefName: "http_3", + scheduledTime: 1713413858858, + startTime: 1713413858917, + endTime: 1713413858928, + updateTime: 1713413858917, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "979c098d-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 1122, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "qixyannkpllogxldfyqi", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 9, + subworkflowChanged: false, + queueWaitTime: 59, + loopOverTask: true, + taskDefinition: null, + }, + ], + }, + http_ref_10: { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_10__1", + retryCount: 0, + seq: 6, + pollCount: 1, + taskDefName: "http_10", + scheduledTime: 1713413857128, + startTime: 1713413857218, + endTime: 1713413857230, + updateTime: 1713413857218, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9693e83e-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": ["max-age=15724800; includeSubDomains"], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7523, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "rzlozabxrltfkexdijot", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 90, + loopOverTask: true, + taskDefinition: null, + loopOver: [ + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_10__1", + retryCount: 0, + seq: 6, + pollCount: 1, + taskDefName: "http_10", + scheduledTime: 1713413857128, + startTime: 1713413857218, + endTime: 1713413857230, + updateTime: 1713413857218, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9693e83e-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7523, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "rzlozabxrltfkexdijot", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 90, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_10__2", + retryCount: 0, + seq: 10, + pollCount: 1, + taskDefName: "http_10", + scheduledTime: 1713413857348, + startTime: 1713413857439, + endTime: 1713413857451, + updateTime: 1713413857439, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "96b5a112-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9295, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "bgnycyjkhkdmiqsnjpcm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 2, + subworkflowChanged: false, + queueWaitTime: 91, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_10__3", + retryCount: 0, + seq: 14, + pollCount: 1, + taskDefName: "http_10", + scheduledTime: 1713413857567, + startTime: 1713413857659, + endTime: 1713413857671, + updateTime: 1713413857659, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "96d70bc6-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 1008, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "drzushsnuxfkfbxjqvfh", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 3, + subworkflowChanged: false, + queueWaitTime: 92, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_10__4", + retryCount: 0, + seq: 18, + pollCount: 1, + taskDefName: "http_10", + scheduledTime: 1713413857810, + startTime: 1713413857904, + endTime: 1713413857915, + updateTime: 1713413857904, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "96fc1ffa-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:37 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 6761, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "taupmlrpyhpzpsulaxqo", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 4, + subworkflowChanged: false, + queueWaitTime: 94, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_10__5", + retryCount: 0, + seq: 22, + pollCount: 1, + taskDefName: "http_10", + scheduledTime: 1713413858034, + startTime: 1713413858126, + endTime: 1713413858139, + updateTime: 1713413858126, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "971e4dfe-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 6970, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "igwwnykfobqdnkxmafab", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 5, + subworkflowChanged: false, + queueWaitTime: 92, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_10__6", + retryCount: 0, + seq: 26, + pollCount: 1, + taskDefName: "http_10", + scheduledTime: 1713413858256, + startTime: 1713413858349, + endTime: 1713413858361, + updateTime: 1713413858349, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97402de2-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8930, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "purhjvrqkogxpcyjifxv", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 6, + subworkflowChanged: false, + queueWaitTime: 93, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_10__7", + retryCount: 0, + seq: 30, + pollCount: 1, + taskDefName: "http_10", + scheduledTime: 1713413858478, + startTime: 1713413858571, + endTime: 1713413858583, + updateTime: 1713413858571, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97620dc6-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5965, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "cozkeartchukblaomchw", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 7, + subworkflowChanged: false, + queueWaitTime: 93, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_10__8", + retryCount: 0, + seq: 34, + pollCount: 1, + taskDefName: "http_10", + scheduledTime: 1713413858712, + startTime: 1713413858806, + endTime: 1713413858818, + updateTime: 1713413858806, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9785c26a-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:38 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9726, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "qmkipozchmypxunkkkzd", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 8, + subworkflowChanged: false, + queueWaitTime: 94, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_10__9", + retryCount: 0, + seq: 38, + pollCount: 1, + taskDefName: "http_10", + scheduledTime: 1713413858936, + startTime: 1713413859029, + endTime: 1713413859040, + updateTime: 1713413859029, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97a7c95e-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2780, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "jklxzsbanazwjffbarxi", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 9, + subworkflowChanged: false, + queueWaitTime: 93, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_10__10", + retryCount: 0, + seq: 42, + pollCount: 1, + taskDefName: "http_10", + scheduledTime: 1713413859158, + startTime: 1713413859250, + endTime: 1713413859260, + updateTime: 1713413859250, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97c9d052-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 944, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "piubltpqkibleiqpmeno", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_10", + taskReferenceName: "http_ref_10", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 92, + loopOverTask: true, + taskDefinition: null, + }, + ], + }, + http_ref_4: { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_4", + retryCount: 0, + seq: 43, + pollCount: 1, + taskDefName: "http_4", + scheduledTime: 1713413859277, + startTime: 1713413859361, + endTime: 1713413859373, + updateTime: 1713413859361, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97dbf8c3-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": ["max-age=15724800; includeSubDomains"], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5081, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "yfxgvcpjtxnjlftbtcpr", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_4", + taskReferenceName: "http_ref_4", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + queueWaitTime: 84, + loopOverTask: false, + taskDefinition: null, + loopOver: [ + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_4", + retryCount: 0, + seq: 43, + pollCount: 1, + taskDefName: "http_4", + scheduledTime: 1713413859277, + startTime: 1713413859361, + endTime: 1713413859373, + updateTime: 1713413859361, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97dbf8c3-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5081, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "yfxgvcpjtxnjlftbtcpr", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_4", + taskReferenceName: "http_ref_4", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + queueWaitTime: 84, + loopOverTask: false, + taskDefinition: null, + }, + ], + }, + do_while_ref_1: { + taskType: "DO_WHILE", + status: "COMPLETED", + inputData: { + number: 10, + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "do_while_ref_1", + retryCount: 0, + seq: 44, + pollCount: 0, + taskDefName: "do_while_1", + scheduledTime: 1713413859382, + startTime: 1713413859382, + endTime: 1713413862160, + updateTime: 1713413861832, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97eb8924-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + 1: { + switch_ref_1: { + evaluationResult: ["2"], + selectedCase: "2", + }, + inline_ref_1: { + result: 2, + }, + http_ref_7: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9943, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "tjxztrsclyzabbqspjmv", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9428, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "lwfynuupwbgkcwgqemcs", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 2: { + switch_ref_1: { + evaluationResult: ["3"], + selectedCase: "3", + }, + inline_ref_1: { + result: 3, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 3723, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "kjmdqmvozujqsxupnlot", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 154, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "obdruesgtvezidzuenhu", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 1892, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "heqrbsinaexemndlvmcp", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 3: { + switch_ref_1: { + evaluationResult: ["1"], + selectedCase: "1", + }, + inline_ref_1: { + result: 1, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7080, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "cuurineeimzdpveetkte", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9931, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "ktrbhjusvnttizwmopxg", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 3569, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "rbeetiujrcnurymrqpvl", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 4: { + switch_ref_1: { + evaluationResult: ["3"], + selectedCase: "3", + }, + inline_ref_1: { + result: 3, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8155, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "tniyursobyqwmcgnfxcj", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5254, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "fqlewtggkleyvghxoyqd", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7061, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "ixireilwipqxtseivuxm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 5: { + switch_ref_1: { + evaluationResult: ["1"], + selectedCase: "1", + }, + inline_ref_1: { + result: 1, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5878, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "bksldazxfxhyoyefvrxf", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2932, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "djokkjixbuwxlsstoaab", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9316, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "dytnpkdddlzmtzlwfpal", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 6: { + switch_ref_1: { + evaluationResult: ["4"], + selectedCase: "4", + }, + inline_ref_1: { + result: 4, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4089, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "khrlsfsjmhvsjgtshbfz", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 7: { + switch_ref_1: { + evaluationResult: ["4"], + selectedCase: "4", + }, + inline_ref_1: { + result: 4, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8433, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "nwltborbttslioxepwhe", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 8: { + switch_ref_1: { + evaluationResult: ["1"], + selectedCase: "1", + }, + inline_ref_1: { + result: 1, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 242, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "idkvujpflbawjvbtcqol", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5914, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "sccygcowawimarjtitnq", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2173, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "saxapbpahdmmtpavvjlm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 9: { + switch_ref_1: { + evaluationResult: ["1"], + selectedCase: "1", + }, + inline_ref_1: { + result: 1, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4359, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "lmbrmrnefvkwwwbmvolm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 395, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "myxiwwnhjbxhdwkfwmzm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2885, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "ykuzzhmdpvfcragcwxoh", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 10: { + switch_ref_1: { + evaluationResult: ["3"], + selectedCase: "3", + }, + inline_ref_1: { + result: 3, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:42 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4274, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "kwmoumdlucxuwgphkxaj", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5175, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "enhhdgrjmtgntufhymzm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:42 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4750, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "oeenqbhrzifhhetyzpcu", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + iteration: 10, + }, + workflowTask: { + name: "do_while_1", + taskReferenceName: "do_while_ref_1", + inputParameters: { + number: 10, + }, + type: "DO_WHILE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopCondition: + "(function () {\n if ($.do_while_ref_1['iteration'] < $.number) {\n return true;\n }\n return false;\n})();", + loopOver: [ + { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + evaluatorType: "graaljs", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + loopOver: [ + { + taskType: "DO_WHILE", + status: "COMPLETED", + inputData: { + number: 10, + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "do_while_ref_1", + retryCount: 0, + seq: 44, + pollCount: 0, + taskDefName: "do_while_1", + scheduledTime: 1713413859382, + startTime: 1713413859382, + endTime: 1713413862160, + updateTime: 1713413861832, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97eb8924-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + 1: { + switch_ref_1: { + evaluationResult: ["2"], + selectedCase: "2", + }, + inline_ref_1: { + result: 2, + }, + http_ref_7: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9943, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "tjxztrsclyzabbqspjmv", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9428, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "lwfynuupwbgkcwgqemcs", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 2: { + switch_ref_1: { + evaluationResult: ["3"], + selectedCase: "3", + }, + inline_ref_1: { + result: 3, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 3723, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "kjmdqmvozujqsxupnlot", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 154, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "obdruesgtvezidzuenhu", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 1892, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "heqrbsinaexemndlvmcp", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 3: { + switch_ref_1: { + evaluationResult: ["1"], + selectedCase: "1", + }, + inline_ref_1: { + result: 1, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7080, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "cuurineeimzdpveetkte", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9931, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "ktrbhjusvnttizwmopxg", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 3569, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "rbeetiujrcnurymrqpvl", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 4: { + switch_ref_1: { + evaluationResult: ["3"], + selectedCase: "3", + }, + inline_ref_1: { + result: 3, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8155, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "tniyursobyqwmcgnfxcj", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5254, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "fqlewtggkleyvghxoyqd", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7061, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "ixireilwipqxtseivuxm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 5: { + switch_ref_1: { + evaluationResult: ["1"], + selectedCase: "1", + }, + inline_ref_1: { + result: 1, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5878, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "bksldazxfxhyoyefvrxf", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2932, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "djokkjixbuwxlsstoaab", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9316, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "dytnpkdddlzmtzlwfpal", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 6: { + switch_ref_1: { + evaluationResult: ["4"], + selectedCase: "4", + }, + inline_ref_1: { + result: 4, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4089, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "khrlsfsjmhvsjgtshbfz", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 7: { + switch_ref_1: { + evaluationResult: ["4"], + selectedCase: "4", + }, + inline_ref_1: { + result: 4, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8433, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "nwltborbttslioxepwhe", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 8: { + switch_ref_1: { + evaluationResult: ["1"], + selectedCase: "1", + }, + inline_ref_1: { + result: 1, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 242, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "idkvujpflbawjvbtcqol", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5914, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "sccygcowawimarjtitnq", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2173, + hostName: "orkes-api-sampler-67dfc8cf58-q2zd8", + randomString: "saxapbpahdmmtpavvjlm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 9: { + switch_ref_1: { + evaluationResult: ["1"], + selectedCase: "1", + }, + inline_ref_1: { + result: 1, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4359, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "lmbrmrnefvkwwwbmvolm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 395, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "myxiwwnhjbxhdwkfwmzm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2885, + hostName: "orkes-api-sampler-67dfc8cf58-nrj8r", + randomString: "ykuzzhmdpvfcragcwxoh", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + 10: { + switch_ref_1: { + evaluationResult: ["3"], + selectedCase: "3", + }, + inline_ref_1: { + result: 3, + }, + http_ref_8: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:42 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4274, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "kwmoumdlucxuwgphkxaj", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_5: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5175, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "enhhdgrjmtgntufhymzm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + http_ref_6: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:42 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4750, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "oeenqbhrzifhhetyzpcu", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + iteration: 10, + }, + workflowTask: { + name: "do_while_1", + taskReferenceName: "do_while_ref_1", + inputParameters: { + number: 10, + }, + type: "DO_WHILE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopCondition: + "(function () {\n if ($.do_while_ref_1['iteration'] < $.number) {\n return true;\n }\n return false;\n})();", + loopOver: [ + { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + evaluatorType: "graaljs", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + ], + }, + inline_ref_1: { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref_1__6", + retryCount: 0, + seq: 69, + pollCount: 0, + taskDefName: "inline_1", + scheduledTime: 1713413860941, + startTime: 1713413860941, + endTime: 1713413860957, + updateTime: 1713413860941, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98d96bad-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 4, + }, + workflowTask: { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 6, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + loopOver: [ + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref_1__1", + retryCount: 0, + seq: 45, + pollCount: 0, + taskDefName: "inline_1", + scheduledTime: 1713413859386, + startTime: 1713413859386, + endTime: 1713413859402, + updateTime: 1713413859386, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97ec2565-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 2, + }, + workflowTask: { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref_1__2", + retryCount: 0, + seq: 49, + pollCount: 0, + taskDefName: "inline_1", + scheduledTime: 1713413859617, + startTime: 1713413859617, + endTime: 1713413859629, + updateTime: 1713413859617, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "980f64d9-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 3, + }, + workflowTask: { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 2, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref_1__3", + retryCount: 0, + seq: 54, + pollCount: 0, + taskDefName: "inline_1", + scheduledTime: 1713413859945, + startTime: 1713413859945, + endTime: 1713413859959, + updateTime: 1713413859945, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9841715e-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 1, + }, + workflowTask: { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 3, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref_1__4", + retryCount: 0, + seq: 59, + pollCount: 0, + taskDefName: "inline_1", + scheduledTime: 1713413860274, + startTime: 1713413860274, + endTime: 1713413860287, + updateTime: 1713413860274, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9873a4f3-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 3, + }, + workflowTask: { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 4, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref_1__5", + retryCount: 0, + seq: 64, + pollCount: 0, + taskDefName: "inline_1", + scheduledTime: 1713413860607, + startTime: 1713413860607, + endTime: 1713413860621, + updateTime: 1713413860607, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98a69bd8-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 1, + }, + workflowTask: { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 5, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref_1__6", + retryCount: 0, + seq: 69, + pollCount: 0, + taskDefName: "inline_1", + scheduledTime: 1713413860941, + startTime: 1713413860941, + endTime: 1713413860957, + updateTime: 1713413860941, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98d96bad-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 4, + }, + workflowTask: { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 6, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref_1__7", + retryCount: 0, + seq: 72, + pollCount: 0, + taskDefName: "inline_1", + scheduledTime: 1713413861052, + startTime: 1713413861052, + endTime: 1713413861070, + updateTime: 1713413861052, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98ea3490-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 4, + }, + workflowTask: { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 7, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref_1__8", + retryCount: 0, + seq: 75, + pollCount: 0, + taskDefName: "inline_1", + scheduledTime: 1713413861161, + startTime: 1713413861161, + endTime: 1713413861175, + updateTime: 1713413861161, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98fafd73-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 1, + }, + workflowTask: { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 8, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref_1__9", + retryCount: 0, + seq: 80, + pollCount: 0, + taskDefName: "inline_1", + scheduledTime: 1713413861496, + startTime: 1713413861497, + endTime: 1713413861510, + updateTime: 1713413861497, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "992e4278-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 1, + }, + workflowTask: { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 9, + subworkflowChanged: false, + queueWaitTime: 1, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "INLINE", + status: "COMPLETED", + inputData: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + _createdBy: "najeeb.thangal@orkes.io", + }, + referenceTaskName: "inline_ref_1__10", + retryCount: 0, + seq: 85, + pollCount: 0, + taskDefName: "inline_1", + scheduledTime: 1713413861829, + startTime: 1713413861829, + endTime: 1713413861842, + updateTime: 1713413861829, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9960760d-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + result: 3, + }, + workflowTask: { + name: "inline_1", + taskReferenceName: "inline_ref_1", + inputParameters: { + evaluatorType: "graaljs", + expression: + "(function () {\n return Math.floor(Math.random() * 4) + 1;\n})();", + }, + type: "INLINE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + ], + }, + switch_ref_1: { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + switchCaseValue: 4, + _createdBy: "najeeb.thangal@orkes.io", + case: "4", + }, + referenceTaskName: "switch_ref_1__6", + retryCount: 0, + seq: 70, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413860963, + startTime: 1713413860963, + endTime: 1713413860963, + updateTime: 1713413860963, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98dcc70e-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["4"], + selectedCase: "4", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 6, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + loopOver: [ + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 2, + _createdBy: "najeeb.thangal@orkes.io", + case: "2", + }, + referenceTaskName: "switch_ref_1__1", + retryCount: 0, + seq: 46, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413859413, + startTime: 1713413859413, + endTime: 1713413859413, + updateTime: 1713413859413, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97ef80c6-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["2"], + selectedCase: "2", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 3, + _createdBy: "najeeb.thangal@orkes.io", + case: "3", + }, + referenceTaskName: "switch_ref_1__2", + retryCount: 0, + seq: 50, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413859637, + startTime: 1713413859637, + endTime: 1713413859637, + updateTime: 1713413859637, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9811fcea-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["3"], + selectedCase: "3", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 2, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 1, + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref_1__3", + retryCount: 0, + seq: 55, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413859968, + startTime: 1713413859968, + endTime: 1713413859968, + updateTime: 1713413859968, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98447e9f-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 3, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 3, + _createdBy: "najeeb.thangal@orkes.io", + case: "3", + }, + referenceTaskName: "switch_ref_1__4", + retryCount: 0, + seq: 60, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413860295, + startTime: 1713413860295, + endTime: 1713413860295, + updateTime: 1713413860295, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98768b24-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["3"], + selectedCase: "3", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 4, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 1, + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref_1__5", + retryCount: 0, + seq: 65, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413860629, + startTime: 1713413860629, + endTime: 1713413860629, + updateTime: 1713413860629, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98a98209-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 5, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + switchCaseValue: 4, + _createdBy: "najeeb.thangal@orkes.io", + case: "4", + }, + referenceTaskName: "switch_ref_1__6", + retryCount: 0, + seq: 70, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413860963, + startTime: 1713413860963, + endTime: 1713413860963, + updateTime: 1713413860963, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98dcc70e-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["4"], + selectedCase: "4", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 6, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + switchCaseValue: 4, + _createdBy: "najeeb.thangal@orkes.io", + case: "4", + }, + referenceTaskName: "switch_ref_1__7", + retryCount: 0, + seq: 73, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413861076, + startTime: 1713413861076, + endTime: 1713413861076, + updateTime: 1713413861076, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98ee0521-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["4"], + selectedCase: "4", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 7, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 1, + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref_1__8", + retryCount: 0, + seq: 76, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413861183, + startTime: 1713413861183, + endTime: 1713413861183, + updateTime: 1713413861183, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98fe0ab4-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 8, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 1, + _createdBy: "najeeb.thangal@orkes.io", + case: "1", + }, + referenceTaskName: "switch_ref_1__9", + retryCount: 0, + seq: 81, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413861520, + startTime: 1713413861520, + endTime: 1713413861520, + updateTime: 1713413861520, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "993128a9-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["1"], + selectedCase: "1", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 9, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "SWITCH", + status: "COMPLETED", + inputData: { + hasChildren: "true", + switchCaseValue: 3, + _createdBy: "najeeb.thangal@orkes.io", + case: "3", + }, + referenceTaskName: "switch_ref_1__10", + retryCount: 0, + seq: 86, + pollCount: 0, + taskDefName: "SWITCH", + scheduledTime: 1713413861851, + startTime: 1713413861851, + endTime: 1713413861851, + updateTime: 1713413861851, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "9963d16e-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + outputData: { + evaluationResult: ["3"], + selectedCase: "3", + }, + workflowTask: { + name: "switch_1", + taskReferenceName: "switch_ref_1", + inputParameters: { + switchCaseValue: "${inline_ref_1.output.result}", + }, + type: "SWITCH", + decisionCases: { + 2: [ + { + name: "http_7", + taskReferenceName: "http_ref_7", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + 4: [], + }, + defaultCase: [ + { + name: "http_5", + taskReferenceName: "http_ref_5", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + { + name: "http_6", + taskReferenceName: "http_ref_6", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 0, + loopOverTask: true, + taskDefinition: null, + }, + ], + }, + http_ref_8: { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_8__6", + retryCount: 0, + seq: 71, + pollCount: 1, + taskDefName: "http_8", + scheduledTime: 1713413860970, + startTime: 1713413861025, + endTime: 1713413861036, + updateTime: 1713413861025, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98de269f-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": ["max-age=15724800; includeSubDomains"], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4089, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "khrlsfsjmhvsjgtshbfz", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 6, + subworkflowChanged: false, + queueWaitTime: 55, + loopOverTask: true, + taskDefinition: null, + loopOver: [ + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_8__1", + retryCount: 0, + seq: 48, + pollCount: 1, + taskDefName: "http_8", + scheduledTime: 1713413859490, + startTime: 1713413859583, + endTime: 1713413859594, + updateTime: 1713413859583, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "97fc7918-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 9428, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "lwfynuupwbgkcwgqemcs", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 1, + subworkflowChanged: false, + queueWaitTime: 93, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_8__2", + retryCount: 0, + seq: 53, + pollCount: 1, + taskDefName: "http_8", + scheduledTime: 1713413859823, + startTime: 1713413859916, + endTime: 1713413859927, + updateTime: 1713413859916, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "982f48ed-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:39 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 3723, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "kjmdqmvozujqsxupnlot", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 2, + subworkflowChanged: false, + queueWaitTime: 93, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_8__3", + retryCount: 0, + seq: 58, + pollCount: 1, + taskDefName: "http_8", + scheduledTime: 1713413860156, + startTime: 1713413860248, + endTime: 1713413860259, + updateTime: 1713413860248, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "986218c2-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 7080, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "cuurineeimzdpveetkte", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 3, + subworkflowChanged: false, + queueWaitTime: 92, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_8__4", + retryCount: 0, + seq: 63, + pollCount: 1, + taskDefName: "http_8", + scheduledTime: 1713413860487, + startTime: 1713413860580, + endTime: 1713413860592, + updateTime: 1713413860580, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98949a77-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8155, + hostName: "orkes-api-sampler-67dfc8cf58-nt2wk", + randomString: "tniyursobyqwmcgnfxcj", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 4, + subworkflowChanged: false, + queueWaitTime: 93, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_8__5", + retryCount: 0, + seq: 68, + pollCount: 1, + taskDefName: "http_8", + scheduledTime: 1713413860819, + startTime: 1713413860914, + endTime: 1713413860925, + updateTime: 1713413860914, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98c7433c-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:40 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 5878, + hostName: "orkes-api-sampler-67dfc8cf58-chmzf", + randomString: "bksldazxfxhyoyefvrxf", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 5, + subworkflowChanged: false, + queueWaitTime: 95, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_8__6", + retryCount: 0, + seq: 71, + pollCount: 1, + taskDefName: "http_8", + scheduledTime: 1713413860970, + startTime: 1713413861025, + endTime: 1713413861036, + updateTime: 1713413861025, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98de269f-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4089, + hostName: "orkes-api-sampler-67dfc8cf58-spxdt", + randomString: "khrlsfsjmhvsjgtshbfz", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 6, + subworkflowChanged: false, + queueWaitTime: 55, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_8__7", + retryCount: 0, + seq: 74, + pollCount: 1, + taskDefName: "http_8", + scheduledTime: 1713413861082, + startTime: 1713413861136, + endTime: 1713413861147, + updateTime: 1713413861136, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "98ef64b2-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 8433, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "nwltborbttslioxepwhe", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 7, + subworkflowChanged: false, + queueWaitTime: 54, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_8__8", + retryCount: 0, + seq: 79, + pollCount: 1, + taskDefName: "http_8", + scheduledTime: 1713413861375, + startTime: 1713413861467, + endTime: 1713413861478, + updateTime: 1713413861467, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "991c1a07-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["181"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 242, + hostName: "orkes-api-sampler-67dfc8cf58-tp2s8", + randomString: "idkvujpflbawjvbtcqol", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 8, + subworkflowChanged: false, + queueWaitTime: 92, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_8__9", + retryCount: 0, + seq: 84, + pollCount: 1, + taskDefName: "http_8", + scheduledTime: 1713413861704, + startTime: 1713413861798, + endTime: 1713413861810, + updateTime: 1713413861798, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "994e4d9c-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:41 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4359, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "lmbrmrnefvkwwwbmvolm", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 9, + subworkflowChanged: false, + queueWaitTime: 94, + loopOverTask: true, + taskDefinition: null, + }, + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_8__10", + retryCount: 0, + seq: 89, + pollCount: 1, + taskDefName: "http_8", + scheduledTime: 1713413862045, + startTime: 1713413862132, + endTime: 1713413862145, + updateTime: 1713413862132, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "998255f1-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:42 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 4274, + hostName: "orkes-api-sampler-67dfc8cf58-7ckpj", + randomString: "kwmoumdlucxuwgphkxaj", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_8", + taskReferenceName: "http_ref_8", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 10, + subworkflowChanged: false, + queueWaitTime: 87, + loopOverTask: true, + taskDefinition: null, + }, + ], + }, + http_ref_9: { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_9", + retryCount: 0, + seq: 90, + pollCount: 1, + taskDefName: "http_9", + scheduledTime: 1713413862163, + startTime: 1713413862244, + endTime: 1713413862258, + updateTime: 1713413862244, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "99945752-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": ["max-age=15724800; includeSubDomains"], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:42 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2042, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "xwadanvnxdolxjqnchsa", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_9", + taskReferenceName: "http_ref_9", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + queueWaitTime: 81, + loopOverTask: false, + taskDefinition: null, + loopOver: [ + { + taskType: "HTTP", + status: "COMPLETED", + inputData: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + _createdBy: "najeeb.thangal@orkes.io", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + referenceTaskName: "http_ref_9", + retryCount: 0, + seq: 90, + pollCount: 1, + taskDefName: "http_9", + scheduledTime: 1713413862163, + startTime: 1713413862244, + endTime: 1713413862258, + updateTime: 1713413862244, + startDelayInSeconds: 0, + retried: false, + executed: true, + callbackFromWorker: true, + responseTimeoutSeconds: 0, + workflowInstanceId: "965f1c98-fd3a-11ee-8cfe-9245dc979bb3", + workflowType: "najeeb_test_do_while_iteration", + taskId: "99945752-fd3a-11ee-8cfe-9245dc979bb3", + callbackAfterSeconds: 0, + workerId: "orkes-workers-deployment-d57759b85-h87xg", + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Thu, 18 Apr 2024 04:17:42 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2042, + hostName: "orkes-api-sampler-67dfc8cf58-psd9l", + randomString: "xwadanvnxdolxjqnchsa", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + workflowTask: { + name: "http_9", + taskReferenceName: "http_ref_9", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + }, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 0, + workflowPriority: 0, + iteration: 0, + subworkflowChanged: false, + queueWaitTime: 81, + loopOverTask: false, + taskDefinition: null, + }, + ], + }, +}; diff --git a/ui-next/src/pages/execution/state/services.ts b/ui-next/src/pages/execution/state/services.ts new file mode 100644 index 0000000000..ee4575ece3 --- /dev/null +++ b/ui-next/src/pages/execution/state/services.ts @@ -0,0 +1,174 @@ +import { fetchWithContext } from "plugins/fetch"; +import { + ExecutionMachineContext, + RestartExecutionEvent, + RetryExecutionEvent, + UpdateVariablesEvent, +} from "./types"; +import { getErrors } from "utils"; +import { fetchExecution } from "commonServices"; +import { toMaybeQueryString } from "utils/toMaybeQueryString"; +import { maybeTriggerFailureWorkflow } from "utils/maybeTriggerWorkflow"; + +export { fetchExecution }; + +export const restartExecution = async ( + { executionId, authHeaders }: ExecutionMachineContext, + event: any, +) => { + const { options = {} } = event as RestartExecutionEvent; + const url = `/workflow/${executionId}/restart${toMaybeQueryString(options)}`; + + try { + const result = await fetchWithContext( + url, + {}, + { + method: "POST", + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + }, + ); + return result; + } catch (error) { + const errorDetails = await getErrors(error as Response); + return Promise.reject({ originalError: error, errorDetails }); + } +}; + +const defaultRetryOptions = { + retryIfRetriedByParent: "false", +}; + +export const retryExecution = async ( + { executionId, authHeaders }: ExecutionMachineContext, + event: any, +) => { + const { options = {} } = event as RetryExecutionEvent; + + const retryOptions = { + ...options, + ...defaultRetryOptions, + }; + + const url = `/workflow/${executionId}/retry${toMaybeQueryString( + retryOptions, + )}`; + + try { + const result = await fetchWithContext( + url, + {}, + { + method: "POST", + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + }, + ); + return result; + } catch (error) { + const errorDetails = await getErrors(error as Response); + return Promise.reject({ originalError: error, errorDetails }); + } +}; +export const terminateExecution = async ({ + executionId, + authHeaders, +}: ExecutionMachineContext) => { + const url = `/workflow/${executionId}${maybeTriggerFailureWorkflow()}`; + try { + const result = await fetchWithContext( + url, + {}, + { + method: "DELETE", + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + }, + ); + return result; + } catch (error) { + const errorDetails = await getErrors(error as Response); + return Promise.reject({ originalError: error, errorDetails }); + } +}; + +export const resumeExecution = async ({ + executionId, + authHeaders, +}: ExecutionMachineContext) => { + const url = `/workflow/${executionId}/resume`; + try { + const result = await fetchWithContext( + url, + {}, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + }, + ); + return result; + } catch (error) { + const errorDetails = await getErrors(error as Response); + return Promise.reject({ originalError: error, errorDetails }); + } +}; + +export const pauseExecution = async ({ + executionId, + authHeaders, +}: ExecutionMachineContext) => { + const url = `/workflow/${executionId}/pause`; + try { + const result = await fetchWithContext( + url, + {}, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + }, + ); + return result; + } catch (error) { + const errorDetails = await getErrors(error as Response); + return Promise.reject({ originalError: error, errorDetails }); + } +}; + +export const updateVariables = async ( + { executionId, authHeaders }: ExecutionMachineContext, + event: UpdateVariablesEvent, +) => { + const url = `/workflow/${executionId}/variables`; + + try { + const result = await fetchWithContext( + url, + {}, + { + method: "POST", + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + body: event.data, + }, + ); + return result; + } catch (error) { + const errorDetails = await getErrors(error as Response); + return Promise.reject({ originalError: error, errorDetails }); + } +}; diff --git a/ui-next/src/pages/execution/state/types.ts b/ui-next/src/pages/execution/state/types.ts new file mode 100644 index 0000000000..84915c7a48 --- /dev/null +++ b/ui-next/src/pages/execution/state/types.ts @@ -0,0 +1,275 @@ +import { ActorRef, DoneInvokeEvent } from "xstate"; +import { FlowEvents, SelectNodeEvent } from "components/flow/state/types"; +import { + TaskDef, + ExecutedData, + TaskType, + AuthHeaders, + WorkflowDef, + User, + WorkflowExecution, + DoWhileSelection, + ExecutionTask, +} from "types"; +import { StatusMap } from "./StatusMapTypes"; +import { SetSelectedTaskEvent } from "../RightPanel/state/types"; + +export enum COUNT_DOWN_TYPE { + INFINITE = -1, +} + +export enum CountdownEventTypes { + TICK = "TICK", + DISABLE = "DISABLE", + ENABLE = "ENABLE", + FORCE_FINISH = "FORCE_FINISH", + UPDATE_DURATION = "UPDATE_DURATION", +} + +export enum ExecutionActionTypes { + UPDATE_EXECUTION = "updateExecution", + REFETCH = "refetch", + EXPAND_DYNAMIC_TASK = "expandDynamicTask", + COLLAPSE_DYNAMIC_TASK = "collapseDynamicTask", + CLEAR_ERROR = "clearError", + RESTART_EXECUTION = "restartExecution", + RETRY_EXECUTION = "retryExecution", + TERMINATE_EXECUTION = "terminateExecution", + RESUME_EXECUTION = "resumeExecution", + PAUSE_EXECUTION = "pauseExecution", + CHANGE_EXECUTION_TAB = "CHANGE_EXECUTION_TAB", + UPDATE_DURATION = "UPDATE_DURATION", + FETCH_FOR_LOGS = "FETCH_FOR_LOGS", + CLOSE_RIGHT_PANEL = "CLOSE_RIGHT_PANEL", + EXECUTION_UPDATED = "EXECUTION_UPDATED", + REPORT_FLOW_ERROR = "REPORT_FLOW_ERROR", + UPDATE_VARIABLES = "UPDATE_VARIABLES", + UPDATE_TASKID_IN_URL = "UPDATE_TASK_ID_IN_URL", + SET_DO_WHILE_ITERATION = "SET_DO_WHILE_ITERATION", + UPDATE_SELECTED_TASK = "UPDATE_SELECTED_TASK", + UPDATE_QUERY_PARAM = "UPDATE_QUERY_PARAM", + TOGGLE_ASSISTANT_PANEL = "TOGGLE_ASSISTANT_PANEL", +} + +export enum ExecutionTabs { + DIAGRAM_TAB = "diagram", + TASK_LIST_TAB = "taskList", + TIMELINE_TAB = "timeLine", + WORKFLOW_INTROSPECTION = "workflowIntrospection", + SUMMARY_TAB = "summary", + WORKFLOW_INPUT_OUTPUT_TAB = "workflowInputOutput", + JSON_TAB = "json", + VARIABLES_TAB = "variables", + TASKS_TO_DOMAIN_TAB = "tasksToDomain", +} +export interface CountdownContext { + duration: number; + elapsed: number; + executionStatus?: string; + countdownType?: COUNT_DOWN_TYPE; + isDisabled?: boolean; +} + +type TickEvent = { + type: CountdownEventTypes.TICK; +}; + +type DisableEvent = { + type: CountdownEventTypes.DISABLE; +}; + +type EnableEvent = { + type: CountdownEventTypes.ENABLE; +}; + +type ForceEndEvent = { + type: CountdownEventTypes.FORCE_FINISH; +}; + +export type UpdateDurationEvent = { + type: + | ExecutionActionTypes.UPDATE_DURATION + | CountdownEventTypes.UPDATE_DURATION; + duration?: number; + countdownType?: COUNT_DOWN_TYPE; + isDisabled?: boolean; +}; + +export type CountdownEvents = + | TickEvent + | DisableEvent + | EnableEvent + | ForceEndEvent + | UpdateDurationEvent + | { type: "" }; // TODO use always instead since this is deprecated + +export enum ErrorSeverity { + INFO = "info", + ERROR = "error", +} + +export enum MessageSeverity { + SUCCESS = "success", +} + +export interface TaskDefExecutionContext extends TaskDef { + executionData: ExecutedData; + forkTasks?: Array; + decisionCases?: Record; + loopOver?: TaskDefExecutionContext[]; + type: TaskType; +} + +export interface WorkflowDefExecutionContext extends WorkflowDef { + tasks: TaskDefExecutionContext[]; +} + +export type ErrorType = { + severity: ErrorSeverity; + text: string; +}; + +export type MessageType = { + severity: MessageSeverity; + text: string; +}; + +export interface ExecutionMachineContext { + execution?: WorkflowExecution; + executionId?: string; + flowChild?: ActorRef; + expandedDynamic: string[]; + workflowDefinition?: Partial; + executionStatusMap?: StatusMap; + error?: ErrorType; + authHeaders?: AuthHeaders; + currentTab: ExecutionTabs; + duration?: number; + countdownType?: COUNT_DOWN_TYPE; + isDisabledCountdown?: boolean; + currentUserInfo?: User; + message?: MessageType; + doWhileSelection?: DoWhileSelection[]; + selectedTask?: ExecutionTask; + selectedTaskReferenceName?: string; + selectedTaskId?: string; + isAssistantPanelOpen: boolean; +} + +export type UpdateExecutionEvent = { + type: ExecutionActionTypes.UPDATE_EXECUTION; + executionId: string; +}; + +export type ClearErrorEvent = { + type: ExecutionActionTypes.CLEAR_ERROR; +}; + +export type PersistErrorEvent = { + type: ExecutionActionTypes.REPORT_FLOW_ERROR; + text: string; + severity: ErrorSeverity; +}; + +export type RefetchEvent = { + type: ExecutionActionTypes.REFETCH; +}; + +export type ExpandDynamicTaskEvent = { + type: ExecutionActionTypes.EXPAND_DYNAMIC_TASK; + taskReferenceName: string; +}; + +export type CollapseDynamicTaskEvent = { + type: ExecutionActionTypes.COLLAPSE_DYNAMIC_TASK; + taskReferenceName: string; +}; + +export type SetDoWhileIterationEvent = { + type: ExecutionActionTypes.SET_DO_WHILE_ITERATION; + data: DoWhileSelection; +}; + +export type RestartExecutionEvent = { + type: ExecutionActionTypes.RESTART_EXECUTION; + options?: Record; +}; + +export type RetryExecutionEvent = { + type: ExecutionActionTypes.RETRY_EXECUTION; + options?: Record; +}; + +export type TerminateExecutionEvent = { + type: ExecutionActionTypes.TERMINATE_EXECUTION; +}; + +export type ResumeExecutionEvent = { + type: ExecutionActionTypes.RESUME_EXECUTION; +}; + +export type PauseExecutionEvent = { + type: ExecutionActionTypes.PAUSE_EXECUTION; +}; + +export type ChangeExecutionTabEvent = { + type: ExecutionActionTypes.CHANGE_EXECUTION_TAB; + tab: ExecutionTabs; +}; + +export type CloseRightPanelEvent = { + type: ExecutionActionTypes.CLOSE_RIGHT_PANEL; +}; + +export type FetchForLogsEvent = { + type: ExecutionActionTypes.FETCH_FOR_LOGS; +}; + +export type ExecutionUpdatedEvent = { + type: ExecutionActionTypes.EXECUTION_UPDATED; +}; + +export type UpdateVariablesEvent = { + type: ExecutionActionTypes.UPDATE_VARIABLES; + data: string; +}; + +export type UpdateSelectedTaskEvent = { + type: ExecutionActionTypes.UPDATE_SELECTED_TASK; + selectedTask: ExecutionTask; +}; + +export type UpdateQueryParamEvent = { + type: ExecutionActionTypes.UPDATE_QUERY_PARAM; + taskReferenceName: string; +}; + +export type ToggleAssistantPanelEvent = { + type: ExecutionActionTypes.TOGGLE_ASSISTANT_PANEL; +}; + +export type ExecutionMachineEvents = + | UpdateExecutionEvent + | ExecutionUpdatedEvent + | RefetchEvent + | ChangeExecutionTabEvent + | UpdateDurationEvent + | ExpandDynamicTaskEvent + | CloseRightPanelEvent + | CollapseDynamicTaskEvent + | SelectNodeEvent + | ClearErrorEvent + | RestartExecutionEvent + | RetryExecutionEvent + | TerminateExecutionEvent + | ResumeExecutionEvent + | PauseExecutionEvent + | FetchForLogsEvent + | SetSelectedTaskEvent + | PersistErrorEvent + | UpdateVariablesEvent + | SetDoWhileIterationEvent + | UpdateSelectedTaskEvent + | UpdateQueryParamEvent + | ToggleAssistantPanelEvent + | DoneInvokeEvent; diff --git a/ui-next/src/pages/execution/timeline.scss b/ui-next/src/pages/execution/timeline.scss new file mode 100644 index 0000000000..02c7943c78 --- /dev/null +++ b/ui-next/src/pages/execution/timeline.scss @@ -0,0 +1,58 @@ +@mixin barColor($colorfg, $colorbg: #fff) { + background-color: $colorbg; + border-color: $colorfg; + color: $colorfg; +} + +.vis-timeline { + margin: 20px; + border-radius: 5px; +} + +.vis-panel { + &.vis-top, + &.vis-center { + border-left: none; + cursor: pointer; + } +} +.vis-label { + .vis-inner { + margin-left: 5px; + min-height: 40px; + } + &.vis-nested-group.vis-group-level-2 { + background: white; + } +} + +.vis-item { + &.status_COMPLETED { + @include barColor(#0a3812, #9fdcaa); + } + &.status_COMPLETED_WITH_ERRORS { + @include barColor(#8b5b02, #feeac5); + } + &.status_IN_PROGRESS, + &.status_SCHEDULED { + @include barColor(#11497a, #8de0f9); + } + //&.status_CANCELED { @include barColor(#26194b, #ded5f8); } + &.status_FAILED, + &.status_FAILED_WITH_TERMINAL_ERROR, + &.status_TIMED_OUT, + &.status_DF_PARTIAL, + &.status_CANCELED { + @include barColor(#7f050b, #fbb4c6); + } + &.status_SKIPPED { + @include barColor(gray); + } + &.vis-selected { + filter: brightness(70%); + } + .vis-item-content { + font-size: 10px; + padding: 0px 3px 0px 3px; + } +} diff --git a/ui-next/src/pages/execution/timelineUtils.ts b/ui-next/src/pages/execution/timelineUtils.ts new file mode 100644 index 0000000000..b1347d1c99 --- /dev/null +++ b/ui-next/src/pages/execution/timelineUtils.ts @@ -0,0 +1,153 @@ +import _first from "lodash/first"; +import _flow from "lodash/flow"; +import _identity from "lodash/identity"; +import _last from "lodash/last"; +import { durationRenderer, juxt, timestampRenderer } from "utils"; +import { ExecutionTask } from "types/Execution"; + +// Define types for the timeline data structures +interface TimelineGroup { + id: string; + content: string; + treeLevel?: number; + nestedGroups?: string[]; +} + +interface TimelineItem { + id: string; + group: string; + content: string; + start: Date; + end: Date; + title: string; + className: string; + style?: string; +} + +type ExecutionStatusMap = Record; + +const extractGroupsAndItems = juxt( + _flow([ + _first, + (gMap: Map) => Array.from(gMap.values()), + ]), // groups to array of values + _flow([_last, _identity]), // don't modify items +); + +function truncate(val: string | undefined): string { + const maxLabelLength = 20; + if (val?.length && val.length > maxLabelLength + 3) { + return val.substring(0, maxLabelLength) + "..."; + } + return val || ""; +} + +// Extract the core logic for testing +export const processTasksToGroupsAndItems = ( + tasks: ExecutionTask[], + executionStatusMap: ExecutionStatusMap, +): [TimelineGroup[], TimelineItem[]] => { + const [groupMap, itemsList] = tasks.reduce( + ( + acc: [Map, TimelineItem[]], + t: ExecutionTask, + ): [Map, TimelineItem[]] => { + const [gc, ic] = acc; + const group: TimelineGroup = { + id: t.referenceTaskName, + content: `${truncate(t.referenceTaskName)} (${truncate( + t.workflowTask.name, + )})`, + ...(executionStatusMap[t.workflowTask.taskReferenceName]?.related == + null + ? {} + : { treeLevel: 2 }), + }; + let item: TimelineItem | TimelineItem[] = []; + if ((t.startTime && t.startTime > 0) || (t.endTime && t.endTime > 0)) { + const startTime = + t.startTime && t.startTime > 0 + ? new Date(t.startTime) + : new Date(t.endTime!); + + const endTime = + t.endTime && t.endTime > 0 + ? new Date(t.endTime) + : new Date(t.startTime!); + + const scheduledTime = t.scheduledTime + ? new Date(t.scheduledTime) + : null; + const duration = durationRenderer( + endTime.getTime() - startTime.getTime(), + ); + + item = { + id: t.taskId!, + group: t.referenceTaskName, + content: `${duration}`, + start: startTime, + end: endTime, + title: `${t.referenceTaskName} (${ + t.status + })
    ${timestampRenderer(startTime.getTime())} - ${timestampRenderer( + endTime.getTime(), + )}`, + className: `status_${t.status}`, + }; + + // Add scheduled time range as a separate item if scheduledTime is available + if (scheduledTime && scheduledTime < startTime) { + const scheduledDuration = durationRenderer( + startTime.getTime() - scheduledTime.getTime(), + ); + const scheduledItem: TimelineItem = { + id: `${t.taskId}_scheduled`, + group: t.referenceTaskName, + content: scheduledDuration, + start: scheduledTime, + end: startTime, + title: `Queue Wait Time: ${scheduledDuration}
    ${timestampRenderer(scheduledTime.getTime())} - ${timestampRenderer(startTime.getTime())}`, + className: "status_SCHEDULED", + style: "background-color: #ffb74d; opacity: 0.7;", + }; + ic.push(scheduledItem); + } + } + gc.set(t.referenceTaskName, group); + return [gc, ic.concat(Array.isArray(item) ? item : [item])]; + }, + [new Map(), [] as TimelineItem[]], + ); + + // Now process FORK_JOIN_DYNAMIC groups to set up nested groups correctly + const groupsArray = Array.from(groupMap.values()); + groupsArray.forEach((group: TimelineGroup) => { + const task = tasks.find( + (t: ExecutionTask) => t.referenceTaskName === group.id, + ); + if ( + task?.workflowTask.type === "FORK_JOIN_DYNAMIC" && + task.inputData?.forkedTasks + ) { + group.nestedGroups = task.inputData.forkedTasks.map((taskId: string) => { + // Check if the group exists as-is first + if (groupMap.has(taskId)) { + return taskId; + } + // If not, try with __iteration suffix + const suffixedId = `${taskId}__${task?.iteration}`; + if (groupMap.has(suffixedId)) { + return suffixedId; + } + // If neither exists, return the original (will cause error but preserves original behavior) + return taskId; + }); + } + }); + + return extractGroupsAndItems([groupMap, itemsList]) as [ + TimelineGroup[], + TimelineItem[], + ]; +}; diff --git a/ui-next/src/pages/executions/ApiSearchModalIntegration.tsx b/ui-next/src/pages/executions/ApiSearchModalIntegration.tsx new file mode 100644 index 0000000000..e960bfcdfc --- /dev/null +++ b/ui-next/src/pages/executions/ApiSearchModalIntegration.tsx @@ -0,0 +1,101 @@ +import { ApiSearchModal } from "components/v1/ApiSearchModal/ApiSearchModal"; +import { toCodeT, useParamsToSdk } from "shared/CodeModal/hook"; +import { SupportedDisplayTypes } from "shared/CodeModal/types"; +import { curlHeaders } from "shared/CodeModal/curlHeader"; + +export type BuildQueryOutput = { + query: string; + freeText: string; + start: number; + size: number; + sort: string; +}; + +interface ApiSearchModalIntegrationProps { + buildQueryOutput: BuildQueryOutput; + onClose: () => void; +} + +const buildEndpoint = ({ + start, + size, + sort, + freeText, + query, +}: BuildQueryOutput) => + `${window.location.origin}/api/workflow/search?${new URLSearchParams({ + start: String(start), + size: String(size), + sort, + freeText, + query, + }).toString()}`; + +const buildCurlCode = ( + buildQueryOutput: BuildQueryOutput, + accessToken: string, +) => { + const endpoint = buildEndpoint(buildQueryOutput); + + const headers = curlHeaders(accessToken); + + const curlCommand = `curl '${endpoint}' \\${Object.entries(headers) + .map(([key, value]) => `\n-H '${key}: ${value}' \\`) + .join("")}\n--compressed`; + + return curlCommand; +}; + +const buildJsCode = ( + buildQueryOutput: BuildQueryOutput, + accessToken: string, +) => { + return `import { orkesConductorClient, WorkflowExecutor } from "@io-orkes/conductor-javascript"; + +async function searchExecution( + start = ${buildQueryOutput.start}, + size = ${buildQueryOutput.size}, + query = "${buildQueryOutput.query}", + freeText = "${buildQueryOutput.freeText}", + sort = "${buildQueryOutput.sort}" +) { + const client = await orkesConductorClient({ + TOKEN: "${accessToken}", + serverUrl: "${window.location.origin}/api" + }); + const executor = new WorkflowExecutor(client); + const results = await executor.search(start, size, query, freeText, sort ); + + return results; + } + + searchExecution(); + `; +}; + +const toCodeMap: toCodeT = { + curl: buildCurlCode, + javascript: buildJsCode, +}; + +const ApiSearchModalIntegration = ({ + onClose, + buildQueryOutput, +}: ApiSearchModalIntegrationProps) => { + const { selectedLanguage, setSelectedLanguage, code } = + useParamsToSdk(buildQueryOutput, toCodeMap); + + return ( + { + setSelectedLanguage(val); + }} + languages={Object.keys(toCodeMap) as SupportedDisplayTypes[]} + /> + ); +}; + +export { ApiSearchModalIntegration }; diff --git a/ui-next/src/pages/executions/BulkActionModule.tsx b/ui-next/src/pages/executions/BulkActionModule.tsx new file mode 100644 index 0000000000..91e82cfae4 --- /dev/null +++ b/ui-next/src/pages/executions/BulkActionModule.tsx @@ -0,0 +1,249 @@ +import React, { SyntheticEvent, useState } from "react"; +import { + Box, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Tab, + Tabs, + Typography, +} from "@mui/material"; +import { useAction } from "utils/query"; +import { maybeTriggerFailureWorkflow } from "utils/maybeTriggerWorkflow"; +import { + Button, + DataTable, + DropdownButton, + Heading, + LinearProgress, +} from "components"; +import executionsStyles from "./executionsStyles"; +import { useAuth } from "shared/auth"; + +interface TabPanelProps { + children?: React.ReactNode; + index: number; + value: number; +} + +function TabPanel(props: TabPanelProps) { + const { children, value, index, ...other } = props; + + return ( + + ); +} + +export default function BulkActionModule({ + selectedRows, + refetchExecution, + handleError, +}: { + selectedRows: any[]; + refetchExecution: () => void; + handleError: (error: any) => void; +}) { + const { isTrialExpired } = useAuth(); + const selectedIds = selectedRows.map((row) => row.workflowId); + const [results, setResults] = useState(null); + const [tab, setTab] = useState(0); + + const { mutate: pauseAction, isLoading: pauseLoading } = useAction( + `/workflow/bulk/pause`, + "put", + { onSuccess, onError }, + ); + const { mutate: resumeAction, isLoading: resumeLoading } = useAction( + `/workflow/bulk/resume`, + "put", + { onSuccess, onError }, + ); + const { mutate: restartCurrentAction, isLoading: restartCurrentLoading } = + useAction(`/workflow/bulk/restart`, "post", { onSuccess }); + const { mutate: restartLatestAction, isLoading: restartLatestLoading } = + useAction(`/workflow/bulk/restart?useLatestDefinitions=true`, "post", { + onSuccess, + onError, + }); + const { mutate: retryAction, isLoading: retryLoading } = useAction( + `/workflow/bulk/retry`, + "post", + { onSuccess, onError }, + ); + const { mutate: terminateAction, isLoading: terminateLoading } = useAction( + `/workflow/bulk/terminate${maybeTriggerFailureWorkflow()}`, + "post", + { onSuccess, onError }, + ); + + const isLoading = + pauseLoading || + resumeLoading || + restartCurrentLoading || + restartLatestLoading || + retryLoading || + terminateLoading; + + function onSuccess(data: any) { + const retval = { + bulkErrorResults: Object.entries(data.bulkErrorResults).map( + ([key, value]) => ({ + workflowId: key, + message: value, + }), + ), + bulkSuccessfulResults: data.bulkSuccessfulResults.map( + (value: string) => ({ + workflowId: value, + }), + ), + }; + setResults(retval); + } + + function onError(error: any) { + handleError(error); + } + + function handleClose() { + setResults(null); + setTab(0); + refetchExecution(); + } + + const handleTabChange = (_event: SyntheticEvent, newValue: number) => { + setTab(newValue); + }; + + return ( + + {selectedRows.length} Workflows Selected. + {/*@ts-ignore*/} + pauseAction({ body: JSON.stringify(selectedIds) }), + }, + { + label: "Resume", + // @ts-ignore + handler: () => resumeAction({ body: JSON.stringify(selectedIds) }), + }, + { + label: "Restart with current definitions", + handler: () => + // @ts-ignore + restartCurrentAction({ body: JSON.stringify(selectedIds) }), + }, + { + label: "Restart with latest definitions", + handler: () => + // @ts-ignore + restartLatestAction({ body: JSON.stringify(selectedIds) }), + }, + { + label: "Retry", + // @ts-ignore + handler: () => retryAction({ body: JSON.stringify(selectedIds) }), + }, + { + label: "Terminate", + handler: () => + // @ts-ignore + terminateAction({ body: JSON.stringify(selectedIds) }), + }, + ]} + > + Bulk Action + + {(results || isLoading) && ( + + + Batch Actions + + + {isLoading && } + {results && ( + + + + + + + + + 15} + /> + + + 15} + /> + + + )} + + + + + + )} + + ); +} diff --git a/ui-next/src/pages/executions/DateControlComponent.tsx b/ui-next/src/pages/executions/DateControlComponent.tsx new file mode 100644 index 0000000000..7e2e752881 --- /dev/null +++ b/ui-next/src/pages/executions/DateControlComponent.tsx @@ -0,0 +1,388 @@ +import { + Box, + IconButton, + Tooltip, + TooltipProps, + Typography, + styled, +} from "@mui/material"; +import { DatePickerComponent } from "./DatePickerComponent"; +import MuiTypography from "components/MuiTypography"; +import CloseOutlinedIcon from "@mui/icons-material/CloseOutlined"; + +import { commonlyUsedDateTime, getSearchDateTime } from "utils/date"; + +import { featureFlags, FEATURES } from "utils/flags"; + +const textStyle = { + fontWeight: "500", + color: "#858585", + fontSize: "13px", +}; + +const timeTextStyle = { + fontWeight: "500", + color: "#1976D2", + fontSize: "13px", + paddingLeft: "5px", + paddingRight: "5px", + cursor: "pointer", +}; + +const CustomisedTooltip = styled(({ className, ...props }: TooltipProps) => ( + +))(() => ({ + "& .MuiTooltip-tooltip": { + backgroundColor: "white", + color: "rgba(6, 6, 6, 1)", + width: "100%", + filter: "drop-shadow(0px 0px 6px rgba(89, 89, 89, 0.41))", + borderRadius: "6px", + padding: "15px 10px 10px 15px", + border: "1px solid #0D94DB", + }, + "& .MuiTooltip-arrow": { + color: "white", + fontSize: "28px", + "&:before": { + border: "1px solid #0D94DB", + }, + }, +})); + +export interface DateControlComponentProps { + startTime: string; + onStartFromChange: (val: string) => void; + startTimeEnd: string; + onStartToChange: (val: string) => void; + endTimeStart: string; + onEndFromChange: (val: string) => void; + endTime: string; + onEndToChange: (val: string) => void; + fromDisplayTime: string; + setFromDisplayTime: (val: string) => void; + toDisplayTime: string; + setToDisplayTime: (val: string) => void; + openDateSelect: boolean; + setOpenDateSelect: (val: boolean) => void; + openStartDatePicker: boolean; + setStartOpenDatePicker: (val: boolean) => void; + openEndDatePicker: boolean; + setEndOpenDatePicker: (val: boolean) => void; + disabled?: boolean; + recentSearches: { start: string; end: string }; + startTimeLabel?: string; + endTimeLabel?: string; + startDialogTitle?: string | null; + startDialogHelpText?: string | null; + endDialogTitle?: string | null; + endDialogHelpText?: string | null; +} + +export const DateControlComponent = ({ + startTime, + onStartFromChange, + startTimeEnd, + onStartToChange, + endTimeStart, + onEndFromChange, + endTime, + onEndToChange, + fromDisplayTime, + setFromDisplayTime, + toDisplayTime, + setToDisplayTime, + setOpenDateSelect, + openStartDatePicker, + setStartOpenDatePicker, + openEndDatePicker, + setEndOpenDatePicker, + startTimeLabel = "Start Time", + endTimeLabel = "End Time", + startDialogTitle = null, + startDialogHelpText = null, + endDialogTitle = null, + endDialogHelpText = null, +}: DateControlComponentProps) => { + const handleCommonStartDate = (time: string) => { + const { rangeStart, rangeEnd } = commonlyUsedDateTime(time); + setFromDisplayTime(getSearchDateTime(rangeStart, rangeEnd)); + onStartFromChange(rangeStart); + onStartToChange(rangeEnd); + }; + + const handleCommonEndDate = (time: string) => { + const { rangeStart, rangeEnd } = commonlyUsedDateTime(time); + setToDisplayTime(getSearchDateTime(rangeStart, rangeEnd)); + onEndFromChange(rangeStart); + onEndToChange(rangeEnd); + }; + + const showEndDatePicker = featureFlags.isEnabled( + FEATURES.SHOW_END_TIME_IN_DATEPICKER, + ); + + return ( + + + + + {startDialogTitle && startDialogHelpText ? ( + + + {startDialogTitle} + + {startDialogHelpText} + + ) : null} + + + + } + > + + { + setStartOpenDatePicker(!openStartDatePicker); + setOpenDateSelect(false); + setEndOpenDatePicker(false); + }} + > + + {startTimeLabel}: + + + {fromDisplayTime} + + + {startTime || startTimeEnd ? ( + { + onStartFromChange(""); + onStartToChange(""); + setFromDisplayTime("Select time range"); + }} + > + + + ) : null} + + + {showEndDatePicker ? ( + + {endDialogTitle && endDialogHelpText ? ( + + + {endDialogTitle} + + {endDialogHelpText} + + ) : null} + + + } + > + + { + setEndOpenDatePicker(!openEndDatePicker); + setOpenDateSelect(false); + setStartOpenDatePicker(false); + }} + > + + {endTimeLabel}: + + + {toDisplayTime} + + + + {endTimeStart || endTime ? ( + { + onEndFromChange(""); + onEndToChange(""); + setToDisplayTime("Select time range"); + }} + > + + + ) : null} + + + ) : null} + +
    +
    + ); +}; diff --git a/ui-next/src/pages/executions/DatePickerComponent.tsx b/ui-next/src/pages/executions/DatePickerComponent.tsx new file mode 100644 index 0000000000..54125a2266 --- /dev/null +++ b/ui-next/src/pages/executions/DatePickerComponent.tsx @@ -0,0 +1,338 @@ +import { + Box, + Grid, + MenuItem, + Tabs, + Tab, + Switch, + IconButton, +} from "@mui/material"; +import MuiButton from "components/MuiButton"; +import MuiTypography from "components/MuiTypography"; +import CheckCircleOutlineOutlinedIcon from "@mui/icons-material/CheckCircleOutlineOutlined"; +import ConductorSelect from "components/v1/ConductorSelect"; +import { COUNT_OPTIONS, TIME_OPTIONS } from "utils/constants/dateTimePicker"; +import { ConductorAutoComplete } from "components/v1"; +import { useState } from "react"; +import { ConductorTimePicker } from "components/v1/date-time/ConductorTimePicker"; +import { SingleDateRangePicker } from "components/v1/date-time/ConductorSingleDateRangePicker"; +import { getCombineDateTime, getDateTime, getSearchDateTime } from "utils/date"; +import _isEmpty from "lodash/isEmpty"; +import XCloseIcon from "components/v1/icons/XCloseIcon"; + +import { COMMONLY_USED } from "utils/constants/dateTimePicker"; + +const blueTextStyle = { + fontWeight: "500", + color: "#1976D2", + fontSize: "13px", +}; + +const headerStyle = { + fontSize: "12px", + fontWeight: 500, + lineHeight: "16px", + padding: "10px 0 5px", +}; + +const selectStyle = { + "& .MuiInputBase-root": { + fontSize: "12px", + }, +}; + +const inputStyle = { + "& .MuiInputBase-root": { + fontSize: "12px", + }, +}; + +const tabStyles = { + "& .MuiTabs-flexContainer": { + justifyContent: "space-between", + borderBottom: "1px solid #DAD9D9", + }, + "& .MuiTab-root": { + color: "#a8a3a3", + fontWeight: 500, + fontSize: "16px", + width: "25%", + }, + "& .MuiTabs-scroller": { + padding: "0 10px", + }, +}; + +const closeIconStyle = { + position: "absolute", + right: "5px", + top: "5px", + cursor: "pointer", +}; + +export interface DatePickerProps { + startDateTime: string; + endDateTime: string; + label: string; + handleFrom: (data: string) => void; + handleTo: (data: string) => void; + openPicker: (val: boolean) => void; + setDisplayName: (val: string) => void; + maxDate: boolean; + handleCommonDate: (time: string) => void; +} + +export const DatePickerComponent = ({ + label, + startDateTime, + endDateTime, + handleFrom, + handleTo, + openPicker, + setDisplayName, + maxDate, + handleCommonDate, +}: DatePickerProps) => { + const [selectedTab, setSelectedTab] = useState(0); + const [roundToMinute, setRoundToMinute] = useState(true); + const [startDate, setStartDate] = useState(startDateTime); + const [endDate, setEndDate] = useState(endDateTime); + const [startTime, setStartTime] = useState(startDateTime); + const [endTime, setEndTime] = useState(endDateTime); + const [count, setCount] = useState("72"); + const [timeUnit, setTimeUnit] = useState("hours"); + const [error, setError] = useState({ start: "", end: "" }); + + const handleDateTime = () => { + const updatedStartDateTime = getCombineDateTime(startDate, startTime); + const updatedEndDateTime = getCombineDateTime(endDate, endTime); + if (updatedEndDateTime < updatedStartDateTime) { + setError({ + start: "", + end: "Start time cannot be greater than the end time.", + }); + } else { + handleFrom(updatedStartDateTime); + handleTo(updatedEndDateTime); + setDisplayName( + getSearchDateTime(updatedStartDateTime, updatedEndDateTime), + ); + openPicker(false); + setError({ start: "", end: "" }); + } + }; + + const handleRelativeTime = () => { + const rangeStartDate = new Date( + getDateTime("last", count, timeUnit, roundToMinute), + ); + handleFrom(rangeStartDate.getTime().toString()); + handleTo(""); + setDisplayName(getSearchDateTime(rangeStartDate.getTime().toString(), "")); + openPicker(false); + }; + + const setStartDateAndTime = (value: string) => { + setStartDate(value); + setStartTime(value); + }; + + const setEndDateAndTime = (value: string) => { + setEndDate(value); + setEndTime(value); + }; + + return ( + + setSelectedTab(val)} + sx={tabStyles} + > + + + + + + openPicker(false)} sx={closeIconStyle}> + + + + {selectedTab === 0 && ( + + + {Object.entries(COMMONLY_USED)?.map(([key, val]) => ( + + { + handleCommonDate(key); + openPicker(false); + }} + sx={{ ...blueTextStyle, cursor: "pointer" }} + > + {val.name} + + + ))} + + + )} + {selectedTab === 1 && ( + + + + + + + + {error.start && ( + + + {error.start} + + + )} + + {error.end && ( + + + {error.end} + + + )} + } + color="primary" + sx={{ position: "absolute", bottom: 0, right: 0 }} + onClick={handleDateTime} + disabled={_isEmpty(endDate)} + > + Apply + + + + + )} + {selectedTab === 2 && ( + + + + setCount(value)} + onInputChange={(__, value) => setCount(value)} + value={count} + freeSolo + disableClearable + sx={inputStyle} + /> + + + setTimeUnit(event.target.value)} + sx={selectStyle} + > + {Object.entries(TIME_OPTIONS).map(([key, val]) => ( + + {val} + + ))} + + + + + setRoundToMinute(!roundToMinute)} + /> + + Round to nearest minute + + + + {(startDateTime || endDateTime) && ( + + {`${label} time`} + + {getSearchDateTime(startDateTime, endDateTime)} + + + )} + + } + color="primary" + onClick={handleRelativeTime} + > + Apply + + + + )} + + ); +}; diff --git a/ui-next/src/pages/executions/ResultsTable.tsx b/ui-next/src/pages/executions/ResultsTable.tsx new file mode 100644 index 0000000000..2ddceb57b4 --- /dev/null +++ b/ui-next/src/pages/executions/ResultsTable.tsx @@ -0,0 +1,345 @@ +import { ReactNode, useEffect, useState } from "react"; +import { DataTable, NavLink, Paper, Text } from "components"; +import { LinearProgress } from "@mui/material"; +import BulkActionModule from "./BulkActionModule"; +import executionsStyles from "./executionsStyles"; +import StatusBadge from "components/StatusBadge"; +import { calculateTimeFromMillis, totalPages } from "utils/utils"; +import { SnackbarMessage } from "components/SnackbarMessage"; +import { ColumnCustomType, LegacyColumn } from "components/DataTable/types"; +import NoDataComponent from "components/NoDataComponent"; +import { colors } from "theme/tokens/variables"; +import { usePushHistory } from "utils/hooks/usePushHistory"; +import { WORKFLOW_EXPLORER_URL } from "utils/constants/route"; + +const LinearIndeterminate = () => { + return ( +
    + +
    + ); +}; + +const executionFields: LegacyColumn[] = [ + { + id: "startTime", + name: "startTime", + type: ColumnCustomType.DATE, + label: "Start Time", + minWidth: "120px", + sortable: true, + tooltip: "The time the workflow was started.", + }, + { + id: "workflowId", + name: "workflowId", + label: "Workflow Id", + grow: 2, + renderer: (workflowId) => ( + {workflowId} + ), + sortable: true, + tooltip: "The unique identifier for the workflow execution.", + }, + { + id: "workflowType", + name: "workflowType", + label: "Workflow Name", + grow: 2, + sortable: true, + tooltip: "The name of the workflow.", + }, + { + id: "version", + name: "version", + label: "Version", + grow: 0.5, + sortable: false, + tooltip: "The version of the workflow.", + }, + { + id: "correlationId", + name: "correlationId", + label: "Correlation Id", + grow: 2, + sortable: false, + tooltip: "The correlation id for the workflow.", + }, + { + id: "idempotencyKey", + name: "idempotencyKey", + label: "Idempotency Key", + grow: 2, + sortable: false, + tooltip: "The idempotency key for the workflow.", + }, + { + id: "updateTime", + name: "updateTime", + label: "Updated Time", + type: ColumnCustomType.DATE, + sortable: true, + tooltip: "The time the workflow was last updated.", + }, + { + id: "endTime", + name: "endTime", + label: "End Time", + type: ColumnCustomType.DATE, + minWidth: "120px", + sortable: true, + tooltip: "The time the workflow was completed.", + }, + { + id: "status", + name: "status", + label: "Status", + sortable: true, + minWidth: "150px", + renderer: (status) => , + tooltip: "The status of the workflow.", + }, + { + id: "input", + name: "input", + label: "Input", + grow: 2, + wrap: true, + sortable: false, + tooltip: "The input for the workflow.", + }, + { + id: "output", + name: "output", + label: "Output", + grow: 2, + sortable: false, + tooltip: "The output for the workflow.", + }, + { + id: "reasonForIncompletion", + name: "reasonForIncompletion", + label: "Reason For Incompletion", + sortable: false, + tooltip: "The reason the workflow was not completed.", + }, + { + id: "executionTime", + name: "executionTime", + label: "Execution Time", + renderer: (time) => { + if (time < 1000) { + return `${time} ms`; + } else { + return calculateTimeFromMillis(Math.floor(time / 1000)); + } + }, + sortable: false, + tooltip: "The time it took to execute the workflow.", + }, + { + id: "event", + name: "event", + label: "Event", + sortable: false, + tooltip: "The event that triggered this workflow.", + }, + { + id: "failedReferenceTaskNames", + name: "failedReferenceTaskNames", + label: "Failed Ref Task Names", + grow: 2, + sortable: false, + tooltip: "The names of the reference tasks that failed.", + }, + { + id: "externalInputPayloadStoragePath", + name: "externalInputPayloadStoragePath", + label: "External Input Payload Storage Path", + sortable: false, + tooltip: "The storage path for the external input payload.", + }, + { + id: "externalOutputPayloadStoragePath", + name: "externalOutputPayloadStoragePath", + label: "External Output Payload Storage Path", + sortable: false, + tooltip: "The storage path for the external output payload.", + }, + { + id: "priority", + name: "priority", + label: "Priority", + sortable: false, + tooltip: "The priority of the workflow.", + }, + + { + id: "createdBy", + name: "createdBy", + label: "Created By", + sortable: false, + tooltip: "The user who created the workflow.", + }, +]; + +export interface ResultsTableProps { + resultObj: any; + error?: any; + busy?: boolean; + page: number; + rowsPerPage: number; + setPage: (page: number) => void; + setSort: (id: string, direction: string) => void; + setRowsPerPage?: (rowsPerPage: number) => void; + showMore?: boolean; + title?: ReactNode; + refetchExecution: () => void; + handleError?: (error: any) => void; + handleClearError?: () => void; + filterOn: boolean; + handleReset: () => void; +} + +export default function ResultsTable({ + resultObj, + error, + busy, + page, + rowsPerPage, + setPage, + setSort, + setRowsPerPage, + title, + refetchExecution, + handleError, + handleClearError, + filterOn, + handleReset, +}: ResultsTableProps) { + const [selectedRows, setSelectedRows] = useState([]); + const [toggleCleared, setToggleCleared] = useState(false); + const pushHistory = usePushHistory(); + + const getErrorMessage = (error: any) => { + return error?.message || error?.statusText; + }; + + useEffect(() => { + setSelectedRows([]); + setToggleCleared((t) => !t); + }, [resultObj]); + + const handleClickBrowseTemplates = () => { + pushHistory(WORKFLOW_EXPLORER_URL); + }; + const handleClickClearSearch = () => { + handleReset(); + }; + + const totalCount = resultObj?.totalHits ?? resultObj?.results?.length; + + return ( + // @ts-ignore + + {error && ( + + )} + {!resultObj && !error && ( + + Click "Search" to submit query. + + )} + } + progressPending={busy} + title={ + title || + ` Page ${page} of ${totalPages( + page, + rowsPerPage.toString(), + resultObj?.results?.length, + )}` + } + data={resultObj?.results ? resultObj?.results : []} + columns={executionFields} + defaultShowColumns={[ + "startTime", + "workflowType", + "workflowId", + "endTime", + "status", + ]} + localStorageKey="workflowSearchExecutions" + keyField="workflowId" + useGlobalRowsPerPage={false} + paginationServer + paginationDefaultPage={page} + paginationPerPage={rowsPerPage} + paginationTotalRows={totalCount} + onChangeRowsPerPage={setRowsPerPage ? setRowsPerPage : undefined} + onChangePage={(page) => setPage(page)} + sortServer + defaultSortAsc={false} + onSort={(column, sortDirection) => { + if (column.id) { + setSort(column.id as string, sortDirection); + } + }} + selectableRows + contextComponent={ + + } + onSelectedRowsChange={({ selectedRows }) => + setSelectedRows(selectedRows) + } + clearSelectedRows={toggleCleared} + customStyles={{ + header: { + style: { + overflow: "visible", + }, + }, + contextMenu: { + style: { + display: "none", + }, + activeStyle: { + display: "flex", + }, + }, + }} + noDataComponent={ + filterOn ? ( + + ) : ( + + ) + } + /> + + ); +} diff --git a/ui-next/src/pages/executions/SchedulerApiSearchModal.tsx b/ui-next/src/pages/executions/SchedulerApiSearchModal.tsx new file mode 100644 index 0000000000..9126c92352 --- /dev/null +++ b/ui-next/src/pages/executions/SchedulerApiSearchModal.tsx @@ -0,0 +1,96 @@ +import { ApiSearchModal } from "components/v1/ApiSearchModal/ApiSearchModal"; +import { curlHeaders } from "shared/CodeModal/curlHeader"; +import { toCodeT, useParamsToSdk } from "shared/CodeModal/hook"; +import { SupportedDisplayTypes } from "shared/CodeModal/types"; +import { BuildQueryOutput } from "./ApiSearchModalIntegration"; + +interface SchedulerApiSearchModalProps { + buildQueryOutput: BuildQueryOutput; + onClose: () => void; +} + +const buildEndpoint = ({ + start, + size, + sort, + freeText, + query, +}: BuildQueryOutput) => + `${ + window.location.origin + }/api/scheduler/search/executions?${new URLSearchParams({ + start: String(start), + size: String(size), + sort, + freeText, + query, + }).toString()}`; + +const buildCurlCode = ( + buildQueryOutput: BuildQueryOutput, + accessToken: string, +) => { + const endpoint = buildEndpoint(buildQueryOutput); + const headers = curlHeaders(accessToken); + const curlCommand = `curl '${endpoint}' \\${Object.entries(headers) + .map(([key, value]) => `\n-H '${key}: ${value}' \\`) + .join("")}\n--compressed`; + + return curlCommand; +}; + +const buildJsCode = ( + buildQueryOutput: BuildQueryOutput, + accessToken: string, +) => { + const { start, size, sort, freeText, query } = buildQueryOutput; + + return `import { orkesConductorClient, SchedulerClient } from "@io-orkes/conductor-javascript"; + +async function searchSchedule( + start = ${start}, + size = ${size}, + sort = "${sort}", + freeText = "${freeText}", + query = "${query}", +) { + const client = await orkesConductorClient({ + TOKEN: "${accessToken}", + serverUrl: "${window.location.origin}/api" + }); + const executor = new SchedulerClient(client); + const results = await executor.search(start, size, sort, freeText, query); + + return results; +} + +searchSchedule(); + `; +}; + +const toCodeMap: toCodeT = { + curl: buildCurlCode, + javascript: buildJsCode, +}; + +const SchedulerApiSearchModal = ({ + onClose, + buildQueryOutput, +}: SchedulerApiSearchModalProps) => { + const { selectedLanguage, setSelectedLanguage, code } = + useParamsToSdk(buildQueryOutput, toCodeMap); + + return ( + { + setSelectedLanguage(val); + }} + languages={Object.keys(toCodeMap) as SupportedDisplayTypes[]} + /> + ); +}; + +export { SchedulerApiSearchModal }; diff --git a/ui-next/src/pages/executions/SchedulerExecutions.tsx b/ui-next/src/pages/executions/SchedulerExecutions.tsx new file mode 100644 index 0000000000..14a7c07bc6 --- /dev/null +++ b/ui-next/src/pages/executions/SchedulerExecutions.tsx @@ -0,0 +1,408 @@ +import { Box, Grid } from "@mui/material"; +import { Button, Paper } from "components"; +import { DEFAULT_ROWS_PER_PAGE } from "components/DataTable/DataTable"; +import StatusBadge from "components/StatusBadge"; +import { renderStatusTagChip } from "components/StatusTagChip"; +import { ConductorAutoComplete } from "components/v1"; +import ConductorInput from "components/v1/ConductorInput"; +import SplitButton from "components/v1/ConductorSplitButton"; +import ConductorDateRangePicker from "components/v1/date-time/ConductorDateRangePicker"; +import ResetIcon from "components/v1/icons/ResetIcon"; +import SearchIcon from "components/v1/icons/SearchIcon"; +import _isEmpty from "lodash/isEmpty"; +import _isEqual from "lodash/isEqual"; +import { useState } from "react"; +import { Helmet } from "react-helmet"; +import { useHotkeys } from "react-hotkeys-hook"; +import { UseQueryResult } from "react-query/types/react/types"; +import { Navigate } from "react-router"; +import { useQueryState } from "react-router-use-location-state"; +import SectionContainer from "shared/SectionContainer"; +import SectionHeader from "shared/SectionHeader"; +import { Key } from "ts-key-enum"; +import { ERROR_URL } from "utils/constants/route"; +import { useScheduleNames } from "utils/hooks/useGetSchedulerDefinitions"; +import { useSchedulerSearch, useWorkflowNames } from "utils/query"; +import { SchedulerApiSearchModal } from "./SchedulerApiSearchModal"; +import SchedulerResultsTable from "./SchedulerResultsTable"; + +const DEFAULT_SORT = "startTime:DESC"; +const MS_IN_DAY = 86400000; + +export default function SchedulerExecutions() { + const [status, setStatus] = useQueryState("status", []); + const [workflowType, setWorkflowType] = useQueryState( + "workflowType", + [], + ); + const [scheduleName, setScheduleName] = useQueryState( + "scheduleName", + [], + ); + const [executionId, setExecutionId] = useQueryState("executionId", ""); + const [startFrom, setStartFrom] = useQueryState("startFrom", ""); + const [startTo, setStartTo] = useQueryState("startTo", ""); + const [lookback, setLookback] = useQueryState("lookback", ""); + + const [errorMessage, setErrorMessage] = useState(null); + + const [page, setPage] = useQueryState("page", 1); + const [rowsPerPage, setRowsPerPage] = useQueryState( + "rowsPerPage", + DEFAULT_ROWS_PER_PAGE, + ); + const [sort, setSort] = useQueryState("sort", DEFAULT_SORT); + const [queryFT, setQueryFT] = useState(buildQuery); + + const [showCodeDialog, setShowCodeDialog] = useQueryState("displayCode", ""); + + const { + data: resultObj, + error, + isFetching, + refetch, + } = useSchedulerSearch({ + page, + rowsPerPage, + sort, + query: queryFT.query, + freeText: queryFT.freeText, + }) as UseQueryResult; + const [unauthorized, setUnauthorized] = useState(null); + + // For dropdown + const workflowNames = useWorkflowNames(); + const scheduleNames = useScheduleNames(); + const scheduleStatuses = ["POLLED", "EXECUTED", "FAILED"]; // POLLED, FAILED, EXECUTED + + function buildQuery() { + const clauses = []; + if (!_isEmpty(workflowType)) { + clauses.push(`workflowType IN (${workflowType.join(",")})`); + } + if (!_isEmpty(scheduleName)) { + clauses.push(`scheduleName IN (${scheduleName.join(",")})`); + } + if (!_isEmpty(executionId)) { + clauses.push(`executionId='${executionId}'`); + } + if (!_isEmpty(status)) { + clauses.push(`status IN (${status.join(",")})`); + } + if (!_isEmpty(lookback)) { + clauses.push( + `startTime>${new Date().getTime() - Number(lookback) * MS_IN_DAY}`, + ); + clauses.push(`startTime<${new Date().getTime()}`); + } + if (!_isEmpty(startFrom)) { + clauses.push(`startTime>${new Date(startFrom).getTime()}`); + } + if (!_isEmpty(startTo)) { + clauses.push(`startTime<${new Date(startTo).getTime()}`); + } + if (!_isEmpty(startFrom) && _isEmpty(startTo)) { + clauses.push(`startTime<${new Date().getTime()}`); + } + return { + query: clauses.join(" AND "), + freeText: "*", + }; + } + + function doSearch() { + setPage(1); + const oldQueryFT = queryFT; + const newQueryFT = buildQuery(); + setQueryFT(newQueryFT); + + // Only force refetch if query didn't change. Else let react-query detect difference and refetch automatically + if (_isEqual(oldQueryFT, newQueryFT)) { + refetch(); + } + } + + // hotkeys to search scheduler execution + useHotkeys(`${Key.Meta}+${Key.Enter}`, doSearch, { + enableOnFormTags: ["INPUT", "TEXTAREA", "SELECT"], + }); + + const handlePage = (page: number) => { + setPage(page); + }; + + const handleSort = (changedColumn: string, direction: string) => { + const sort = `${changedColumn}:${direction.toUpperCase()}`; + setPage(1); + setSort(sort); + doSearch(); + }; + + const handleRowsPerPage = (rowsPerPage: number) => { + setPage(1); + setRowsPerPage(rowsPerPage); + }; + + const handleLookback = (val: string) => { + setStartFrom(""); + setStartTo(""); + setLookback(val); + }; + + const onStartFromChange = (val: string) => { + setLookback(""); + setStartFrom(val); + }; + + const onStartToChange = (val: string) => { + setLookback(""); + setStartTo(val); + }; + + if (error?.status === 401) { + const readJsonResponse = async () => { + try { + const json = await error.json(); + setUnauthorized(json); + } catch { + setUnauthorized(null); + } + }; + readJsonResponse(); + } + + if (unauthorized) { + if (unauthorized?.message) { + return ( + + ); + } + + return ; + } + + const handleError = (error: any) => { + setErrorMessage(error); + }; + const handleClearError = () => { + setErrorMessage(null); + }; + + const clearAllFields = () => { + setWorkflowType([]); + setScheduleName([]); + setExecutionId(""); + setStatus([]); + setLookback(""); + setStartFrom(""); + setStartTo(""); + }; + + const handleReset = () => { + clearAllFields(); + const newQueryFT = { query: "", freeText: "*" }; + setQueryFT(newQueryFT); + refetch(); + }; + + return ( + <> + + Scheduled Workflow Executions + + {showCodeDialog && ( + setShowCodeDialog("")} + buildQueryOutput={{ + start: (page - 1) * rowsPerPage, + size: rowsPerPage, + sort, + freeText: queryFT.freeText, + query: buildQuery().query, + }} + /> + )} + + + + + + setScheduleName(val)} + value={scheduleName} + conductorInputProps={{ + autoFocus: true, + }} + /> + + + + setWorkflowType(val)} + value={workflowType} + /> + + + + + + + + + + + + + + + + setStatus(val)} + value={status} + renderTags={renderStatusTagChip} + renderOption={(props, option) => ( + + + + )} + /> + + + + + + + + } + options={[ + { + label: "Show as code", + onClick: () => setShowCodeDialog("active"), + }, + ]} + primaryOnClick={doSearch} + > + Search + + + + + + + + ); +} diff --git a/ui-next/src/pages/executions/SchedulerResultsTable.tsx b/ui-next/src/pages/executions/SchedulerResultsTable.tsx new file mode 100644 index 0000000000..4cba4532d1 --- /dev/null +++ b/ui-next/src/pages/executions/SchedulerResultsTable.tsx @@ -0,0 +1,265 @@ +import { useEffect, useState } from "react"; +import { DataTable, LinearProgress, NavLink, Paper, Text } from "components"; +import { AlertTitle } from "@mui/material"; +import BulkActionModule from "./BulkActionModule"; +import executionsStyles from "./executionsStyles"; +import ClipboardCopy from "components/ClipboardCopy"; +import StatusBadge from "components/StatusBadge"; +import { SnackbarMessage } from "components/SnackbarMessage"; +import { totalPages } from "utils/index"; +import { ColumnCustomType, LegacyColumn } from "components/DataTable/types"; +import MuiAlert from "components/MuiAlert"; +import NoDataComponent from "components/NoDataComponent"; +import { colors } from "theme/tokens/variables"; +import { usePushHistory } from "utils/hooks/usePushHistory"; +import { SCHEDULER_DEFINITION_URL } from "utils/constants/route"; +import { useAuth } from "shared/auth"; + +const executionFields: LegacyColumn[] = [ + { + id: "scheduledTime", + name: "scheduledTime", + type: ColumnCustomType.DATE, + label: "Scheduled time", + grow: 0.7, + sortable: true, + tooltip: "The time the workflow was scheduled to run.", + }, + { + id: "executionTime", + name: "executionTime", + type: ColumnCustomType.DATE, + label: "Execution time", + grow: 0.7, + sortable: true, + tooltip: "The time the workflow was executed.", + }, + { + id: "executionId", + name: "executionId", + label: "Execution id", + grow: 1.5, + sortable: true, + renderer: (executionId) => ( + {executionId} + ), + tooltip: "The unique identifier for the scheduler execution.", + }, + { + id: "scheduleName", + name: "scheduleName", + label: "Schedule name", + grow: 0.7, + sortable: true, + tooltip: "The name of the schedule.", + }, + { + id: "workflowName", + name: "workflowName", + label: "Workflow name", + grow: 0.7, + sortable: true, + tooltip: "The name of the workflow.", + }, + { + id: "workflowId", + name: "workflowId", + label: "Workflow id", + grow: 1.5, + sortable: true, + renderer: (workflowId) => { + if (!workflowId) { + return ""; + } + return ( + + {workflowId} + + ); + }, + tooltip: "The unique identifier for the workflow execution.", + }, + { + id: "state", + name: "state", + label: "Status", + grow: 0.5, + sortable: false, + renderer: (state) => , + tooltip: "The status of the execution.", + }, + { + id: "reason", + name: "reason", + label: "Reason for failure", + sortable: true, + tooltip: "The reason the execution failed.", + }, + { + id: "stackTrace", + name: "stackTrace", + label: "Error details", + sortable: true, + tooltip: "The error details.", + }, +]; + +export interface SchedulerResultsTableProps { + resultObj: any; + error: any; + busy?: boolean; + page: number; + rowsPerPage: number; + setPage: (page: number) => void; + setSort: (id: string, direction: string) => void; + setRowsPerPage?: (rowsPerPage: number) => void; + refetchExecution: () => void; + errorMessage: any; + handleError: (error: any) => void; + handleClearError: () => void; + isFilterOn: boolean; + handleReset: () => void; +} + +export default function SchedulerResultsTable({ + resultObj, + error, + busy, + page, + rowsPerPage, + setPage, + setSort, + setRowsPerPage, + refetchExecution, + errorMessage, + handleError, + handleClearError, + isFilterOn, + handleReset, +}: SchedulerResultsTableProps) { + const { isTrialExpired } = useAuth(); + const [selectedRows, setSelectedRows] = useState([]); + const [toggleCleared, setToggleCleared] = useState(false); + const pushHistory = usePushHistory(); + + const getErrorMessage = (error: any) => { + return error?.message || error?.statusText; + }; + + useEffect(() => { + setSelectedRows([]); + setToggleCleared((t) => !t); + }, [resultObj]); + + const handleClickDefineSchedule = () => { + pushHistory(SCHEDULER_DEFINITION_URL.NEW); + }; + + return ( + // @ts-ignore + + {busy && } + {error && ( + + Request Failed + {getErrorMessage(error)} + + )} + {errorMessage && ( + + )} + {!resultObj && !error && ( + + Click "Search" to submit query. + + )} + {resultObj && ( + setPage(page)} + sortServer + defaultSortFieldId="scheduledTime" + defaultSortAsc={false} + onSort={(column, sortDirection) => { + setSort(column.id as string, sortDirection); + }} + selectableRows + paginationTotalRows={resultObj?.totalHits} + contextComponent={ + + } + onSelectedRowsChange={({ selectedRows }) => + setSelectedRows(selectedRows) + } + clearSelectedRows={toggleCleared} + customStyles={{ + header: { + style: { + overflow: "visible", + }, + }, + contextMenu: { + style: { + display: "none", + }, + activeStyle: { + display: "flex", + }, + }, + }} + noDataComponent={ + isFilterOn ? ( + + ) : ( + + ) + } + /> + )} + + ); +} diff --git a/ui-next/src/pages/executions/SearchExampleQuery.tsx b/ui-next/src/pages/executions/SearchExampleQuery.tsx new file mode 100644 index 0000000000..c682006663 --- /dev/null +++ b/ui-next/src/pages/executions/SearchExampleQuery.tsx @@ -0,0 +1,34 @@ +import MuiTypography from "components/MuiTypography"; + +export const ExampleSearchQuery = () => { + return ( + + workflowType + = + 'test' + AND + status + in + ( + 'RUNNING' + , + 'COMPLETED' + ) + AND + input + . Age + = + 10 + AND + createdBy + = + 'mail@example.com' + + ); +}; diff --git a/ui-next/src/pages/executions/SplitWorkflowDefinitionButton/ImportBPNFileDialog.tsx b/ui-next/src/pages/executions/SplitWorkflowDefinitionButton/ImportBPNFileDialog.tsx new file mode 100644 index 0000000000..39aa9340b2 --- /dev/null +++ b/ui-next/src/pages/executions/SplitWorkflowDefinitionButton/ImportBPNFileDialog.tsx @@ -0,0 +1,303 @@ +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + Box, + Typography, + IconButton, + InputAdornment, + Stack, + Alert, + FormControlLabel, + Switch, +} from "@mui/material"; +import ConductorInput from "components/v1/ConductorInput"; +import { UploadSimple, XCircle } from "@phosphor-icons/react"; +import CodeBlockInput from "components/CodeBlockInput"; +import { useImportBPMWorkflow } from "./hook"; +import { useRef } from "react"; +import MuiTypography from "components/MuiTypography"; + +export const ImportBPNFileDialog = ({ + open, + onClose, +}: { + open: boolean; + onClose: () => void; +}) => { + const { + onChangeFileContent, + onUpload, + onFileSelect, + onDragEnter, + onDragLeave, + onDragOver, + onDrop, + onReset, + onWorkflowNameChange, + onOverWriteWorkflowToggle, + selectedFile, + fileContent, + isDragging, + isUploading, + uploadError, + workflowName, + workflowNameError, + overWriteWorkflow, + } = useImportBPMWorkflow({ onClose }); + + const fileInputRef = useRef(null); + const handleReset = () => { + onReset(); + onClose(); + }; + return ( + + + + + + + IMPORT BPMN + + + Convert a BPMN file automatically into a workflow definition. + + + + theme.palette.grey[500], + }} + > + + + + + + + + + + + + + ), + readOnly: true, + }} + /> + + + + + OR + + + fileInputRef.current?.click()} + > + {isDragging + ? "Drop BPMN file here" + : "drag & drop BPMN file here"} + + + + OR + + + { + onChangeFileContent(value); + }} + containerStyles={{ + marginTop: "8px", + }} + /> + + onWorkflowNameChange(e.target.value)} + placeholder="Enter workflow name" + error={!!workflowNameError} + helperText={ + workflowNameError || "This will be used as the workflow name" + } + sx={{ mt: 4 }} + /> + + + + Overwrite workflow + + + + When enabled, any existing workflow with the same name will be + overwritten. + + + + } + label="" + /> + + + + + + + {uploadError && ( + + {uploadError} + + )} + + + + + + + ); +}; diff --git a/ui-next/src/pages/executions/SplitWorkflowDefinitionButton/SplitWorkflowDefinitionButton.tsx b/ui-next/src/pages/executions/SplitWorkflowDefinitionButton/SplitWorkflowDefinitionButton.tsx new file mode 100644 index 0000000000..b46b81d7b6 --- /dev/null +++ b/ui-next/src/pages/executions/SplitWorkflowDefinitionButton/SplitWorkflowDefinitionButton.tsx @@ -0,0 +1,77 @@ +import { usePushHistory } from "utils/hooks/usePushHistory"; +import SplitButton from "components/v1/ConductorSplitButton"; +import AddIcon from "components/v1/icons/AddIcon"; +import { + WORKFLOW_DEFINITION_URL, + WORKFLOW_EXPLORER_URL, +} from "utils/constants/route"; +import { useAuth } from "shared/auth"; +import { useMemo, useState } from "react"; +import { ImportBPNFileDialog } from "./ImportBPNFileDialog"; +import { featureFlags, FEATURES } from "utils/flags"; +import { removeCopyFromStorage } from "pages/runWorkflow/runWorkflowUtils"; + +const SplitWorkflowDefinitionButton = ({ + disabled, +}: { + disabled?: boolean; +}) => { + const pushHistory = usePushHistory(); + const { isTrialExpired } = useAuth(); + const [openBPMNModal, setOpenBPMNModal] = useState(false); + const isImportBpmnHidden = featureFlags.isEnabled(FEATURES.HIDE_IMPORT_BPMN); + + const clearNewWorkflowStorage = () => { + // Clear any existing new workflow data from localStorage + removeCopyFromStorage({ + workflowName: "newWorkflowDef", + currentVersion: undefined, + isNewWorkflow: true, + }); + }; + + const splitButtonOptions = useMemo(() => { + const options = [ + { + label: "New Workflow", + onClick: () => { + clearNewWorkflowStorage(); + pushHistory(WORKFLOW_DEFINITION_URL.NEW); + }, + }, + { + label: "Select Template", + onClick: () => pushHistory(WORKFLOW_EXPLORER_URL), + }, + ]; + if (!isImportBpmnHidden) { + options.push({ + label: "Import BPMN", + onClick: () => setOpenBPMNModal(true), + }); + } + return options; + }, [isImportBpmnHidden, pushHistory]); + + return ( + <> + } + options={splitButtonOptions} + primaryOnClick={() => { + clearNewWorkflowStorage(); + pushHistory(WORKFLOW_DEFINITION_URL.NEW); + }} + disabled={disabled || isTrialExpired} + > + Define workflow + + setOpenBPMNModal(false)} + /> + + ); +}; + +export default SplitWorkflowDefinitionButton; diff --git a/ui-next/src/pages/executions/SplitWorkflowDefinitionButton/hook.ts b/ui-next/src/pages/executions/SplitWorkflowDefinitionButton/hook.ts new file mode 100644 index 0000000000..7f355ce437 --- /dev/null +++ b/ui-next/src/pages/executions/SplitWorkflowDefinitionButton/hook.ts @@ -0,0 +1,188 @@ +import { fetchWithContext } from "plugins/fetch"; +import { ChangeEvent, DragEvent, useState } from "react"; +import { WORKFLOW_NAME_ERROR_MESSAGE } from "utils/constants/common"; +import { WORKFLOW_NAME_REGEX } from "utils/constants/regex"; +import { WORKFLOW_DEFINITION_URL } from "utils/constants/route"; +import { usePushHistory } from "utils/hooks/usePushHistory"; +import { useAuthHeaders } from "utils/query"; + +const stripBPMNExtension = (fileName: string): string => { + return fileName.replace(/\.bpmn$/i, ""); +}; + +export const useImportBPMWorkflow = ({ onClose }: { onClose: () => void }) => { + const authHeaders = useAuthHeaders(); + const pushHistory = usePushHistory(); + const [selectedFile, setSelectedFile] = useState(""); + const [workflowName, setWorkflowName] = useState(""); + const [workflowNameError, setWorkflowNameError] = useState( + null, + ); + const [overWriteWorkflow, setOverWriteWorkflow] = useState(true); + const [fileContent, onChangeFileContent] = useState(""); + const [isDragging, setIsDragging] = useState(false); + const [isUploading, setIsUploading] = useState(false); + const [uploadError, setUploadError] = useState(null); + + const onReset = () => { + setSelectedFile(""); + setWorkflowName(""); + onChangeFileContent(""); + setUploadError(null); + }; + + const onUpload = async () => { + setIsUploading(true); + setUploadError(null); + + // Validate XML first + try { + const parser = new DOMParser(); + const xmlDoc = parser.parseFromString(fileContent, "text/xml"); + const parseError = xmlDoc.getElementsByTagName("parsererror").length > 0; + + if (parseError) { + setUploadError("Invalid XML format"); + setIsUploading(false); + return; + } + } catch { + setUploadError("Invalid XML format"); + setIsUploading(false); + return; + } + + try { + const fileName = workflowName.endsWith(".bpmn") + ? workflowName + : `${workflowName}.bpmn`; + const importedWorkflows = await fetchWithContext( + `/metadata/workflow-importer/import-bpm?overwrite=${overWriteWorkflow}`, + {}, + { + method: "POST", + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + body: JSON.stringify({ + fileName, + fileContent, + }), + }, + ); + + if (importedWorkflows?.length === 0) { + setUploadError( + "A workflow with the same name already exists. Please rename the workflow or enable the 'Overwrite workflow' option to proceed.", + ); + } else { + onClose(); + + setSelectedFile(""); + setWorkflowName(""); + onChangeFileContent(""); + const firstWorkflow = importedWorkflows[0]; + if (firstWorkflow) { + pushHistory(WORKFLOW_DEFINITION_URL.BASE + "/" + firstWorkflow.name); + } + } + } catch (err: unknown) { + if (err instanceof Response) { + const errorAsJson = await err.json(); + setUploadError(errorAsJson.message || "Upload failed"); + } else if (err instanceof Error) { + setUploadError(err.message); + } else { + setUploadError("Upload failed"); + } + } finally { + setIsUploading(false); + } + }; + + const onFileSelect = (e: ChangeEvent) => { + setUploadError(null); + const file = e.target.files?.[0]; + if (file) { + setSelectedFile(file.name); + setWorkflowName(stripBPMNExtension(file.name)); + const reader = new FileReader(); + reader.onload = (e) => { + const content = e.target?.result as string; + onChangeFileContent(content); + }; + reader.readAsText(file); + } + }; + + const onDragEnter = (e: DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(true); + }; + + const onDragLeave = (e: DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + }; + + const onDragOver = (e: DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + }; + + const onDrop = (e: DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + setUploadError(null); + + const files = Array.from(e.dataTransfer.files); + const bpmnFile = files.find((file) => file.name.endsWith(".bpmn")); + + if (bpmnFile) { + setSelectedFile(bpmnFile.name); + setWorkflowName(stripBPMNExtension(bpmnFile.name)); + const reader = new FileReader(); + reader.onload = (e) => { + const content = e.target?.result as string; + onChangeFileContent(content); + }; + reader.readAsText(bpmnFile); + } + }; + + const onWorkflowNameChange = (value: string) => { + setWorkflowName(value); + setWorkflowNameError( + WORKFLOW_NAME_REGEX.test(value) ? null : WORKFLOW_NAME_ERROR_MESSAGE, + ); + }; + + const onOverWriteWorkflowToggle = () => { + setOverWriteWorkflow(!overWriteWorkflow); + }; + + return { + onUpload, + onFileSelect, + onDragEnter, + onDragLeave, + onDragOver, + onDrop, + onChangeFileContent, + onReset, + onWorkflowNameChange, + onOverWriteWorkflowToggle, + selectedFile, + workflowName, + fileContent, + isDragging, + isUploading, + uploadError, + workflowNameError, + overWriteWorkflow, + } as const; +}; diff --git a/ui-next/src/pages/executions/Task/AdvanceSearch.tsx b/ui-next/src/pages/executions/Task/AdvanceSearch.tsx new file mode 100644 index 0000000000..34737f096b --- /dev/null +++ b/ui-next/src/pages/executions/Task/AdvanceSearch.tsx @@ -0,0 +1,264 @@ +import { Monaco } from "@monaco-editor/react"; +import { Box, Grid } from "@mui/material"; +import { Button } from "components"; +import MuiTypography from "components/MuiTypography"; +import { ConductorCodeBlockInput } from "components/v1/ConductorCodeBlockInput"; +import ConductorInput from "components/v1/ConductorInput"; +import SplitButton from "components/v1/ConductorSplitButton"; +import ResetIcon from "components/v1/icons/ResetIcon"; +import SearchIcon from "components/v1/icons/SearchIcon"; +import { Dispatch, useEffect, useRef } from "react"; +import { QueryDispatch, SetStateAction } from "react-router-use-location-state"; +import { colors } from "theme/tokens/variables"; +import { TaskType } from "types/common"; +import { TaskStatus } from "types/TaskStatus"; +import { + TASK_SEARCH_QUERY_SUGGESTIONS, + WORKFLOW_SEARCH_QUERY_SUGGESTIONS, +} from "utils/constants/common"; +import { useLocalStorage } from "utils/localstorage"; +import { DateControlComponent } from "../DateControlComponent"; +import { ExampleSearchQuery } from "../SearchExampleQuery"; + +const taskTypes = Object.values(TaskType); +const taskStatuses = Object.values(TaskStatus).sort((a, b) => + a.toLowerCase().localeCompare(b.toLowerCase()), +); + +interface AdvanceSearchComponentProps { + queryText: string; + freeText: string; + startTime: string; + startTimeEnd: string; + fromDisplayTime: string; + endTimeStart: string; + endTime: string; + toDisplayTime: string; + openDateSelect: boolean; + openStartDatePicker: boolean; + setStartOpenDatePicker: Dispatch>; + setOpenDateSelect: Dispatch>; + setToDisplayTime: Dispatch>; + openEndDatePicker: boolean; + setFreeText: QueryDispatch>; + setQueryText: QueryDispatch>; + setShowCodeDialog: QueryDispatch>; + handleReset: () => void; + doSearch: () => void; + onStartFromChange: (val: string) => void; + onStartToChange: (val: string) => void; + onEndFromChange: (val: string) => void; + onEndToChange: (val: string) => void; + setFromDisplayTime: Dispatch>; + setEndOpenDatePicker: Dispatch>; + recentSearches: { start: string; end: string }; +} + +export const AdvanceSearch = ({ + queryText, + freeText, + startTime, + endTime, + setQueryText, + onStartFromChange, + onStartToChange, + setFreeText, + handleReset, + doSearch, + setShowCodeDialog, + toDisplayTime, + setToDisplayTime, + setOpenDateSelect, + setStartOpenDatePicker, + startTimeEnd, + openDateSelect, + endTimeStart, + openEndDatePicker, + fromDisplayTime, + openStartDatePicker, + setFromDisplayTime, + setEndOpenDatePicker, + onEndFromChange, + onEndToChange, + recentSearches, +}: AdvanceSearchComponentProps) => { + const disposeRef = useRef void)>(null); + + useEffect(() => { + return () => { + if (disposeRef.current) { + disposeRef.current(); + } + }; + }, []); + + // for tooltip flag in localstorage + const [tooltipFlags, setTooltipFlags] = useLocalStorage("tooltipFlags", {}); + const handleToolTipOnClose = () => { + if (tooltipFlags && !tooltipFlags.executionSearch) { + setTooltipFlags({ ...tooltipFlags, executionSearch: true }); + } + }; + return ( + + + + Search tasks by query parameters. Then hit ENTER, and now you + can click SEARCH. + + + Sample: + + + +
    + ), + showInitial: !tooltipFlags.executionSearch, + initialTimeout: 2000, + onClose: handleToolTipOnClose, + }} + beforeMount={(monaco: Monaco) => { + if (disposeRef.current) { + disposeRef.current(); + disposeRef.current = null; + } + const disposable = monaco.languages.registerCompletionItemProvider( + "sql", + { + provideCompletionItems: () => { + const propertyKeys = [ + ...WORKFLOW_SEARCH_QUERY_SUGGESTIONS, + ...TASK_SEARCH_QUERY_SUGGESTIONS, + ...taskTypes, + ...taskStatuses, + ]; + + // Provide suggestions for properties that start with the current text + const propertySuggestions = propertyKeys.map((property) => ({ + label: property, + kind: monaco.languages.CompletionItemKind.Value, + insertText: property, + })); + // Merge custom suggestions with property suggestions + const suggestions = [...propertySuggestions]; + return { suggestions }; + }, + }, + ); + // IMPORTANT: keep `dispose()` bound to its disposable context. + // Destructuring `dispose` can lose `this` and throw "Unbound disposable context". + disposeRef.current = () => disposable.dispose(); + }} + options={{ + lineNumbers: "off", + }} + /> + + + + + + + + + + + + + } + options={[ + { + label: "Show as code", + onClick: () => setShowCodeDialog("active"), + }, + ]} + primaryOnClick={doSearch} + > + Search + + + + + ); +}; diff --git a/ui-next/src/pages/executions/Task/BasicSearch.tsx b/ui-next/src/pages/executions/Task/BasicSearch.tsx new file mode 100644 index 0000000000..cd7084ab13 --- /dev/null +++ b/ui-next/src/pages/executions/Task/BasicSearch.tsx @@ -0,0 +1,294 @@ +import { Box, Grid } from "@mui/material"; +import { Button } from "components"; +import StatusBadge from "components/StatusBadge"; +import { renderStatusTagChip } from "components/StatusTagChip"; +import { ConductorAutoComplete } from "components/v1"; +import ConductorInput from "components/v1/ConductorInput"; +import SplitButton from "components/v1/ConductorSplitButton"; +import ResetIcon from "components/v1/icons/ResetIcon"; +import SearchIcon from "components/v1/icons/SearchIcon"; +import { Dispatch } from "react"; +import { QueryDispatch, SetStateAction } from "react-router-use-location-state"; +import { TaskType } from "types/common"; +import { TaskStatus } from "types/TaskStatus"; +import { DateControlComponent } from "../DateControlComponent"; + +const taskTypes = Object.values(TaskType).filter( + (type) => ![TaskType.START, TaskType.SWITCH_JOIN].includes(type), +); +const taskStatuses = Object.values(TaskStatus) + .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())) + .filter((status) => status !== TaskStatus.PENDING); +interface BasicSearchComponentProps { + taskDefName: string; + taskExecutionId: string; + taskRefName: string; + workflowName: string; + freeText: string; + startTime: string; + startTimeEnd: string; + fromDisplayTime: string; + endTimeStart: string; + endTime: string; + status: string[]; + toDisplayTime: string; + openDateSelect: boolean; + openStartDatePicker: boolean; + setStartOpenDatePicker: Dispatch>; + setOpenDateSelect: Dispatch>; + setToDisplayTime: Dispatch>; + taskType: string[]; + openEndDatePicker: boolean; + setTaskDefName: QueryDispatch>; + setTaskExecutionId: QueryDispatch>; + setTaskRefName: QueryDispatch>; + setWorkflowName: QueryDispatch>; + setFreeText: QueryDispatch>; + setStatus: QueryDispatch>; + setTaskType: QueryDispatch>; + setShowCodeDialog: QueryDispatch>; + setFromDisplayTime: Dispatch>; + setEndOpenDatePicker: Dispatch>; + handleReset: () => void; + doSearch: () => void; + onStartFromChange: (val: string) => void; + onStartToChange: (val: string) => void; + onEndFromChange: (val: string) => void; + onEndToChange: (val: string) => void; + queryText: string; + recentSearches: { start: string; end: string }; +} + +export const BasicSearch = ({ + taskDefName, + taskExecutionId, + taskRefName, + workflowName, + freeText, + startTime, + toDisplayTime, + setToDisplayTime, + setOpenDateSelect, + setStartOpenDatePicker, + startTimeEnd, + openDateSelect, + endTime, + endTimeStart, + status, + queryText, + openEndDatePicker, + taskType, + fromDisplayTime, + openStartDatePicker, + setTaskDefName, + setFromDisplayTime, + setTaskExecutionId, + setEndOpenDatePicker, + setTaskRefName, + setWorkflowName, + setFreeText, + setStatus, + setTaskType, + setShowCodeDialog, + handleReset, + doSearch, + onStartFromChange, + onEndFromChange, + onEndToChange, + onStartToChange, + recentSearches, +}: BasicSearchComponentProps) => { + return ( + + + + + + setTaskType(val)} + value={taskType} + /> + + + + + + + + + + + + setStatus(val)} + value={status} + renderTags={renderStatusTagChip} + renderOption={(props, option) => ( + + + + )} + /> + + + + + + + + + + + + + } + options={[ + { + label: "Show as code", + onClick: () => setShowCodeDialog("active"), + }, + ]} + primaryOnClick={doSearch} + > + Search + + + + + ); +}; diff --git a/ui-next/src/pages/executions/Task/SwitchComponent.tsx b/ui-next/src/pages/executions/Task/SwitchComponent.tsx new file mode 100644 index 0000000000..37e104d498 --- /dev/null +++ b/ui-next/src/pages/executions/Task/SwitchComponent.tsx @@ -0,0 +1,32 @@ +import { Box, FormControlLabel, Switch } from "@mui/material"; +import { SetStateAction } from "react"; +import { QueryDispatch } from "react-router-use-location-state"; + +interface SwitchComponentProps { + asQuery: boolean; + setAsQuery: QueryDispatch>; +} + +export const SwitchComponent = ({ + asQuery, + setAsQuery, +}: SwitchComponentProps) => { + return ( + + setAsQuery(!asQuery)} /> + } + label="SQL format" + /> + + ); +}; diff --git a/ui-next/src/pages/executions/Task/TaskApiSearchModal.tsx b/ui-next/src/pages/executions/Task/TaskApiSearchModal.tsx new file mode 100644 index 0000000000..eaa6a86cac --- /dev/null +++ b/ui-next/src/pages/executions/Task/TaskApiSearchModal.tsx @@ -0,0 +1,62 @@ +import { ApiSearchModal } from "components/v1/ApiSearchModal/ApiSearchModal"; +import { curlHeaders } from "shared/CodeModal/curlHeader"; +import { toCodeT, useParamsToSdk } from "shared/CodeModal/hook"; +import { SupportedDisplayTypes } from "shared/CodeModal/types"; +import { BuildQueryOutput } from "../ApiSearchModalIntegration"; + +interface TaskApiSearchModalProps { + buildQueryOutput: BuildQueryOutput; + onClose: () => void; +} + +const buildEndpoint = ({ + start, + size, + sort, + freeText, + query, +}: BuildQueryOutput) => + `${window.location.origin}/api/tasks/search?${new URLSearchParams({ + start: String(start), + size: String(size), + sort, + freeText, + query, + }).toString()}`; + +const buildCurlCode = ( + buildQueryOutput: BuildQueryOutput, + accessToken: string, +) => { + const endpoint = buildEndpoint(buildQueryOutput); + const headers = curlHeaders(accessToken); + const curlCommand = `curl '${endpoint}' \\${Object.entries(headers) + .map(([key, value]) => `\n-H '${key}: ${value}' \\`) + .join("")}\n--compressed`; + + return curlCommand; +}; + +const toCodeMap: toCodeT = { + curl: buildCurlCode, +}; + +export const TaskApiSearchModal = ({ + onClose, + buildQueryOutput, +}: TaskApiSearchModalProps) => { + const { selectedLanguage, setSelectedLanguage, code } = + useParamsToSdk(buildQueryOutput, toCodeMap); + + return ( + { + setSelectedLanguage(val); + }} + languages={Object.keys(toCodeMap) as SupportedDisplayTypes[]} + /> + ); +}; diff --git a/ui-next/src/pages/executions/TaskResultsTable.tsx b/ui-next/src/pages/executions/TaskResultsTable.tsx new file mode 100644 index 0000000000..c319ae5f3e --- /dev/null +++ b/ui-next/src/pages/executions/TaskResultsTable.tsx @@ -0,0 +1,359 @@ +import { LinearProgress } from "@mui/material"; +import { ReactNode, useEffect, useState } from "react"; + +import { DataTable, NavLink, Paper, Text } from "components"; +import { ColumnCustomType, LegacyColumn } from "components/DataTable/types"; +import { usePushHistory } from "utils/hooks/usePushHistory"; +import NoDataComponent from "components/NoDataComponent"; +import { SnackbarMessage } from "components/SnackbarMessage"; +import StatusBadge from "components/StatusBadge"; +import { colors } from "theme/tokens/variables"; +import { + WORKFLOW_DEFINITION_URL, + WORKFLOW_EXECUTION_URL, + WORKFLOW_EXPLORER_URL, +} from "utils/constants/route"; +import { calculateTimeFromMillis, totalPages } from "utils/utils"; +import BulkActionModule from "./BulkActionModule"; +import executionsStyles from "./executionsStyles"; +import { toMaybeQueryString } from "utils/toMaybeQueryString"; + +const LinearIndeterminate = () => { + return ( +
    + +
    + ); +}; + +const executionFields: LegacyColumn[] = [ + { + id: "startTime", + name: "startTime", + type: ColumnCustomType.DATE, + label: "Start Time", + sortable: true, + minWidth: "120px", + }, + { + id: "endTime", + name: "endTime", + label: "End Time", + type: ColumnCustomType.DATE, + minWidth: "120px", + sortable: true, + }, + { + id: "taskId", + name: "taskId", + label: "Task execution Id", + grow: 2, + renderer: (taskId, row) => { + const urlParameters = { + taskId: row.taskId, + }; + return ( + + {taskId} + + ); + }, + sortable: true, + }, + { + id: "taskDefName", + name: "taskDefName", + label: "Task name", + sortable: false, + }, + { + id: "scheduledTime", + name: "scheduledTime", + label: "Scheduled time", + type: ColumnCustomType.DATE, + minWidth: "120px", + sortable: true, + }, + { + id: "taskType", + name: "taskType", + label: "Task Type", + sortable: true, + }, + { + id: "taskReferenceName", + name: "taskReferenceName", + label: "Task Reference Name", + sortable: true, + }, + { + id: "workflowType", + name: "workflowType", + label: "Workflow Name", + sortable: true, + grow: 2, + renderer: (workflowName) => ( + + {workflowName} + + ), + }, + { + id: "updateTime", + name: "updateTime", + label: "Updated Time", + type: ColumnCustomType.DATE, + sortable: true, + }, + + { + id: "status", + name: "status", + label: "Status", + sortable: true, + minWidth: "150px", + renderer: (status) => , + }, + { + id: "input", + name: "input", + label: "Input", + grow: 2, + wrap: true, + sortable: false, + }, + { id: "output", name: "output", label: "Output", grow: 2, sortable: false }, + { + id: "executionTime", + name: "executionTime", + label: "Execution Time", + renderer: (time) => { + if (time < 1000) { + return `${time} ms`; + } else { + return calculateTimeFromMillis(Math.floor(time / 1000)); + } + }, + sortable: false, + }, + { + id: "workflowId", + name: "workflowId", + label: "Workflow Id", + renderer: (executionId) => ( + + {executionId} + + ), + sortable: true, + }, + { + id: "workflowPriority", + name: "workflowPriority", + label: "Workflow priority", + sortable: false, + }, + { + id: "correlationId", + name: "correlationId", + label: "Correlation id", + sortable: false, + }, + { + id: "reasonForIncompletion", + name: "reasonForIncompletion", + label: "Reason for Incompletion", + sortable: false, + }, + { + id: "queueWaitTime", + name: "queueWaitTime", + label: "Queue Wait Time", + sortable: false, + }, + { + id: "externalInputPayloadStoragePath", + name: "externalInputPayloadStoragePath", + label: "External Input Payload Storage Path", + sortable: false, + }, + { + id: "externalOutputPayloadStoragePath", + name: "externalOutputPayloadStoragePath", + label: "External Output Payload Storage Path", + sortable: false, + }, +]; + +export interface ResultsTableProps { + resultObj: any; + error?: any; + busy?: boolean; + page: number; + rowsPerPage: number; + setPage: (page: number) => void; + setSort: (id: string, direction: string) => void; + setRowsPerPage?: (rowsPerPage: number) => void; + showMore?: boolean; + title?: string | ReactNode; + refetchExecution: () => void; + handleError?: (error: any) => void; + handleClearError?: () => void; + filterOn: boolean; + handleReset: () => void; +} + +export default function ResultsTable({ + resultObj, + error, + busy, + page, + rowsPerPage, + setPage, + setSort, + setRowsPerPage, + title, + refetchExecution, + handleError, + handleClearError, + filterOn, + handleReset, +}: ResultsTableProps) { + const [selectedRows, setSelectedRows] = useState([]); + const [toggleCleared, setToggleCleared] = useState(false); + const pushHistory = usePushHistory(); + + const getErrorMessage = (error: any) => { + return error?.message || error?.statusText; + }; + + useEffect(() => { + setSelectedRows([]); + setToggleCleared((t) => !t); + }, [resultObj]); + + const handleClickBrowseTemplates = () => { + pushHistory(WORKFLOW_EXPLORER_URL); + }; + const handleClickClearSearch = () => { + handleReset(); + }; + + const totalCount = resultObj?.totalHits ?? resultObj?.results?.length; + + return ( + + {error && ( + + )} + {!resultObj && !error && ( + + Click "Search" to submit query. + + )} + + } + progressPending={busy} + pagination + paginationServer + paginationTotalRows={totalCount} + title={ + title || + ` Page ${page} of ${totalPages( + page, + rowsPerPage.toString(), + resultObj?.results?.length, + )}` + } + data={resultObj?.results ? resultObj?.results : []} + columns={executionFields} + defaultShowColumns={[ + "startTime", + "endTime", + "taskId", + "taskType", + "scheduledTime", + "workflowType", + "status", + ]} + localStorageKey="taskSearchExecutions" + keyField="taskId" // paginationServer + useGlobalRowsPerPage={false} + paginationDefaultPage={page} + paginationPerPage={rowsPerPage} + onChangeRowsPerPage={setRowsPerPage} + onChangePage={(page) => setPage(page)} + hideSearch + sortServer + defaultSortAsc={false} + onSort={(column, sortDirection) => { + if (column.id) { + setSort(column.id as string, sortDirection); + } + }} + selectableRows + contextComponent={ + + } + onSelectedRowsChange={({ selectedRows }) => + setSelectedRows(selectedRows) + } + clearSelectedRows={toggleCleared} + customStyles={{ + header: { + style: { + overflow: "visible", + }, + }, + contextMenu: { + style: { + display: "none", + }, + activeStyle: { + display: "flex", + }, + }, + }} + noDataComponent={ + filterOn ? ( + + ) : ( + + ) + } + /> + + ); +} diff --git a/ui-next/src/pages/executions/TaskSearch.tsx b/ui-next/src/pages/executions/TaskSearch.tsx new file mode 100644 index 0000000000..1f5d054b8f --- /dev/null +++ b/ui-next/src/pages/executions/TaskSearch.tsx @@ -0,0 +1,478 @@ +import { Box } from "@mui/material"; +import { Paper } from "components"; +import { DEFAULT_ROWS_PER_PAGE } from "components/DataTable/DataTable"; +import MuiTypography from "components/MuiTypography"; +import AddIcon from "components/v1/icons/AddIcon"; +import _isEmpty from "lodash/isEmpty"; +import _isEqual from "lodash/isEqual"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { Helmet } from "react-helmet"; +import { useHotkeys } from "react-hotkeys-hook"; +import { UseQueryResult } from "react-query"; +import { Navigate } from "react-router"; +import { useQueryState } from "react-router-use-location-state"; +import SectionContainer from "shared/SectionContainer"; +import SectionHeader from "shared/SectionHeader"; +import SectionHeaderActions from "shared/SectionHeaderActions"; +import { colors } from "theme/tokens/variables"; +import { Key } from "ts-key-enum"; +import { TaskExecutionResult } from "types/TaskExecution"; +import { IObject } from "types/common"; +import { dateToEpoch } from "utils"; +import { ERROR_URL, NEW_TASK_DEF_URL } from "utils/constants/route"; +import { commonlyUsedDateTime, getSearchDateTime } from "utils/date"; +import { usePushHistory } from "utils/hooks/usePushHistory"; +import { useTaskExecutionsSearch } from "utils/query"; +import { getErrors, tryToJson } from "utils/utils"; +import { AdvanceSearch } from "./Task/AdvanceSearch"; +import { BasicSearch } from "./Task/BasicSearch"; +import { SwitchComponent } from "./Task/SwitchComponent"; +import { TaskApiSearchModal } from "./Task/TaskApiSearchModal"; +import ResultsTable from "./TaskResultsTable"; + +const DEFAULT_SORT = "startTime:DESC"; + +const getTableTitle = (resultObj: TaskExecutionResult) => { + const { results, totalHits } = resultObj; + return ( + + + {results.length} results + + + of {totalHits} + + + ); +}; + +export function TaskSearch() { + const currentTimeStamp = Date.now().toString(); + const last72HoursTimestamp = Date.now() - 72 * 60 * 60 * 1000; + + const [freeText, setFreeText] = useQueryState("freeText", ""); + const [taskDefName, setTaskDefName] = useQueryState("taskDefName", ""); + const [taskId, setTaskId] = useQueryState("taskId", ""); + const [taskRefName, setTaskRefName] = useQueryState("taskRefName", ""); + const [workflowName, setWorkflowName] = useQueryState("workflowName", ""); + const [queryText, setQueryText] = useQueryState("query", ""); + const [status, setStatus] = useQueryState("status", []); + const [taskType, setTaskType] = useQueryState("taskType", []); + + const [startTimeFrom, setStartTimeFrom] = useQueryState( + "startFrom", + commonlyUsedDateTime("last72Hours").rangeStart, + ); + + const [startTimeEnd, setStartTimeEnd] = useQueryState("startTimeTo", ""); + const [endTimeFrom, setEndTimeFrom] = useQueryState("endTimeFrom", ""); + const [endTimeTo, setEndTime] = useQueryState("endTimeTo", ""); + + const [page, setPage] = useQueryState("page", 1); + const [rowsPerPage, setRowsPerPage] = useQueryState( + "rowsPerPage", + DEFAULT_ROWS_PER_PAGE, + ); + const [sort, setSort] = useQueryState("sort", DEFAULT_SORT); + const [showCodeDialog, setShowCodeDialog] = useQueryState("displayCode", ""); + const [asQuery, setAsQuery] = useQueryState("asQuery", false); + const [errorMessage, setErrorMessage] = useState(null); + + const [unauthorized, setUnauthorized] = useState<{ + message?: string; + error?: string; + } | null>(null); + + const [openDateSelect, setOpenDateSelect] = useState(false); + const [openStartDatePicker, setStartOpenDatePicker] = useState(false); + const [openEndDatePicker, setEndOpenDatePicker] = useState(false); + const [fromDisplayTime, setFromDisplayTime] = useState( + startTimeFrom + ? getSearchDateTime(startTimeFrom, startTimeEnd) + : "Last 72 Hours", + ); + const [toDisplayTime, setToDisplayTime] = useState( + endTimeTo ? getSearchDateTime(endTimeFrom, endTimeTo) : "Select time range", + ); + + const recentSearches = + (tryToJson(localStorage.getItem("recentTaskSearch")) as { + start: string; + end: string; + }) || {}; + + useEffect(() => { + if (!startTimeFrom) { + setStartTimeFrom(last72HoursTimestamp.toString()); + setStartTimeEnd(""); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const buildQuery = useCallback(() => { + const clauses = []; + + if (asQuery) { + if (!_isEmpty(queryText)) { + clauses.push(queryText); + } + } else { + if (!_isEmpty(taskDefName)) { + clauses.push(`taskDefName='${taskDefName}'`); + } + if (!_isEmpty(taskType) && !queryText.includes("taskType")) { + clauses.push(`taskType IN (${taskType.join(",")})`); + } + if (!_isEmpty(taskId)) { + clauses.push(`taskId='${taskId}'`); + } + if (!_isEmpty(taskRefName)) { + clauses.push(`referenceTaskName='${taskRefName}'`); + } + if (!_isEmpty(workflowName)) { + clauses.push(`workflowName='${workflowName}'`); + } + if (!_isEmpty(status) && !queryText.includes("status")) { + clauses.push(`status IN (${status.join(",")})`); + } + } + if (!_isEmpty(startTimeFrom)) { + clauses.push(`startTime>${dateToEpoch(startTimeFrom)}`); + } + if (!_isEmpty(startTimeEnd)) { + clauses.push(`startTime<${dateToEpoch(startTimeEnd)}`); + } + if (!_isEmpty(endTimeFrom)) { + clauses.push(`endTime>${dateToEpoch(endTimeFrom)}`); + } + if (!_isEmpty(endTimeTo)) { + clauses.push(`endTime<${dateToEpoch(endTimeTo)}`); + } + + return { + query: clauses.join(" AND "), + freeText: _isEmpty(freeText) ? "*" : freeText, + }; + }, [ + asQuery, + endTimeTo, + endTimeFrom, + freeText, + queryText, + startTimeFrom, + startTimeEnd, + status, + taskDefName, + taskId, + taskRefName, + taskType, + workflowName, + ]); + + const [queryFT, setQueryFT] = useState(buildQuery); + const { + data: resultObj, + error, + isFetching, + refetch, + }: UseQueryResult = useTaskExecutionsSearch( + { + page, + rowsPerPage, + sort, + query: queryFT.query, + freeText: queryFT.freeText, + }, + { + onError: (error: any) => { + if (error) { + getErrors(error as Response).then((result) => { + if (result?.["taskNames"] === "must not be empty") { + setErrorMessage({ message: "task name should not be empty" }); + } else { + setErrorMessage(result); + } + }); + } else { + setErrorMessage(null); + } + }, + }, + ); + + const doSearch = useCallback(() => { + setPage(1); + + const oldQueryFT = queryFT; + const newQueryFT = buildQuery(); + setQueryFT(newQueryFT); + + if (_isEqual(oldQueryFT, newQueryFT)) { + refetch(); + } + if (startTimeFrom || startTimeEnd || endTimeFrom || endTimeTo) { + localStorage.setItem( + "recentTaskSearch", + JSON.stringify({ + start: startTimeFrom || startTimeEnd, + end: endTimeTo || endTimeFrom, + }), + ); + } + }, [ + buildQuery, + endTimeTo, + queryFT, + refetch, + setPage, + startTimeFrom, + startTimeEnd, + endTimeFrom, + ]); + + // hotkeys to search execution + useHotkeys(`${Key.Meta}+${Key.Enter}`, doSearch, { + enableOnFormTags: ["INPUT", "TEXTAREA", "SELECT"], + }); + + const handlePage = (page: number) => { + setPage(page); + }; + + const handleSort = (changedColumn: string, direction: string) => { + const sortColumn = + changedColumn === "workflowType" ? "workflowName" : changedColumn; + const sort = `${sortColumn}:${direction.toUpperCase()}`; + setPage(1); + setSort(sort); + }; + + const handleRowsPerPage = (rowsPerPage: number) => { + setPage(1); + setRowsPerPage(rowsPerPage); + }; + + const onStartFromChange = (val: string) => { + if (val) setStartTimeFrom(String(dateToEpoch(val))); + else setStartTimeFrom(""); + }; + + const onStartToChange = (val: string) => { + if (val) setStartTimeEnd(String(dateToEpoch(val))); + else setStartTimeEnd(""); + }; + + const pushHistory = usePushHistory(); + + // Must be called before any early returns to follow Rules of Hooks + const filterOn = useMemo(() => { + if (queryFT.query !== "" || queryFT.freeText !== "*") { + return true; + } else { + return false; + } + }, [queryFT]); + + // @ts-ignore + if (error?.status === 401) { + const errorResult = error; + const parseErrorResponse = async () => { + try { + // @ts-ignore + const json = await errorResult.clone().json(); + setUnauthorized(json); + } catch { + setUnauthorized(null); + } + }; + parseErrorResponse(); + } + + if (unauthorized) { + if (unauthorized.message) { + return ( + + ); + } + + return ; + } + + const handleError = (error: any) => { + setErrorMessage(error); + }; + const handleClearError = () => { + setErrorMessage(null); + }; + + const onEndFromChange = (val: string) => { + if (val) setEndTimeFrom(String(dateToEpoch(val))); + else setEndTimeFrom(""); + }; + + const onEndToChange = (val: string) => { + if (val) setEndTime(String(dateToEpoch(val))); + else setEndTime(""); + }; + + const clearAllFields = () => { + if (asQuery) { + setQueryText(""); + } else { + setTaskDefName(""); + setTaskType([]); + setTaskId(""); + setTaskRefName(""); + setWorkflowName(""); + setStatus([]); + } + setStartTimeFrom(last72HoursTimestamp.toString()); + setStartTimeEnd(""); + setEndTimeFrom(""); + setEndTime(""); + setFreeText(""); + setToDisplayTime(""); + setFromDisplayTime("Last 72 Hours"); + setSort(DEFAULT_SORT); + }; + + const handleReset = () => { + clearAllFields(); + const newQueryFT = { + query: `startTime>${last72HoursTimestamp.toString()} AND startTime<${currentTimeStamp}`, + freeText: "*", + }; + setQueryFT(newQueryFT); + }; + + return ( + <> + + Task Executions + + + {showCodeDialog && ( + setShowCodeDialog("")} + buildQueryOutput={{ + start: (page - 1) * rowsPerPage, + size: rowsPerPage, + sort, + freeText, + query: buildQuery().query, + }} + /> + )} + pushHistory(NEW_TASK_DEF_URL), + startIcon: , + }, + ]} + /> + } + /> + + + + {asQuery ? ( + + ) : ( + + )} + + + + + + ); +} diff --git a/ui-next/src/pages/executions/WorkflowSearch.tsx b/ui-next/src/pages/executions/WorkflowSearch.tsx new file mode 100644 index 0000000000..c5c38cb8cb --- /dev/null +++ b/ui-next/src/pages/executions/WorkflowSearch.tsx @@ -0,0 +1,238 @@ +import { Box, FormControlLabel, Switch } from "@mui/material"; +import MuiTypography from "components/MuiTypography"; +import PlayIcon from "components/v1/icons/PlayIcon"; +import _isEqual from "lodash/isEqual"; +import { useEffect, useState } from "react"; +import { Helmet } from "react-helmet"; +import { useQueryState } from "react-router-use-location-state"; +import SectionContainer from "shared/SectionContainer"; +import SectionHeader from "shared/SectionHeader"; +import SectionHeaderActions from "shared/SectionHeaderActions"; +import { colors } from "theme/tokens/variables"; +import { TaskExecutionResult } from "types/TaskExecution"; +import { DoSearchProps } from "types/WorkflowExecution"; +import { RUN_WORKFLOW_URL } from "utils/constants/route"; +import { dateToEpoch } from "utils/date"; +import { commonlyUsedDateTime, getSearchDateTime } from "utils/date"; +import { usePushHistory } from "utils/hooks/usePushHistory"; +import { tryToJson } from "utils/utils"; +import SplitWorkflowDefinitionButton from "./SplitWorkflowDefinitionButton/SplitWorkflowDefinitionButton"; +import AdvancedSearch from "./workflowSearchComponents/AdvancedSearch"; +import BasicSearch from "./workflowSearchComponents/BasicSearch"; + +const SwitchComponent = ({ + asQuery, + setAsQuery, +}: { + asQuery: boolean; + setAsQuery: (value: boolean) => void; +}) => ( + + setAsQuery(!asQuery)} />} + label="SQL format" + /> + +); + +export default function WorkflowPanel() { + const [asQuery, setAsQuery] = useQueryState("asQuery", false); + const [freeText, setFreeText] = useQueryState("freeText", ""); + const [status, setStatus] = useQueryState("status", []); + const [openDateSelect, setOpenDateSelect] = useState(false); + const [openStartDatePicker, setStartOpenDatePicker] = useState(false); + const [openEndDatePicker, setEndOpenDatePicker] = useState(false); + const [startTimeFrom, setStartTimeFrom] = useQueryState( + "startFrom", + commonlyUsedDateTime("last72Hours").rangeStart, + ); + const [startTimeTo, setStartTimeTo] = useQueryState("startTo", ""); + const [endTimeFrom, setEndTimeFrom] = useQueryState("endTimeFrom", ""); + const [endTimeTo, setEndTimeTo] = useQueryState("endTimeTo", ""); + const [fromDisplayTime, setFromDisplayTime] = useState( + startTimeFrom + ? getSearchDateTime(startTimeFrom, startTimeTo) + : "Last 72 Hours", + ); + const [toDisplayTime, setToDisplayTime] = useState( + endTimeTo ? getSearchDateTime(endTimeFrom, endTimeTo) : "Select time range", + ); + + const last72HoursTimestamp = Date.now() - 72 * 60 * 60 * 1000; + + const recentSearches = + (tryToJson(localStorage.getItem("recentTaskSearch")) as { + start: string; + end: string; + }) || {}; + + useEffect(() => { + if (!startTimeFrom) { + setStartTimeFrom(last72HoursTimestamp.toString()); + setStartTimeTo(""); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const onStartFromChange = (val: string) => { + setStartTimeFrom(val ? String(dateToEpoch(val)) : ""); + }; + const onStartToChange = (val: string) => { + setStartTimeTo(val ? String(dateToEpoch(val)) : ""); + }; + const onEndFromChange = (val: string) => { + setEndTimeFrom(val ? String(dateToEpoch(val)) : ""); + }; + const onEndToChange = (val: string) => { + setEndTimeTo(val ? String(dateToEpoch(val)) : ""); + }; + + const doSearch = ({ + queryFT, + buildQuery, + setQueryFT, + refetch, + setPage, + setRecentTaskSearch, + }: DoSearchProps) => { + setPage(1); + const oldQueryFT = queryFT; + const newQueryFT = buildQuery(); + setQueryFT(newQueryFT); + + if (_isEqual(oldQueryFT, newQueryFT)) { + refetch(); + } + setRecentTaskSearch?.(); + }; + + const pushHistory = usePushHistory(); + + const getTableTitle = (resultObj: TaskExecutionResult) => { + const { results, totalHits } = resultObj; + return ( + + + {results.length} results + + + of {totalHits} + + + ); + }; + + return ( + <> + + Workflow Executions + + pushHistory(RUN_WORKFLOW_URL), + startIcon: , + }, + { + customButtonElement: , + }, + ]} + /> + } + /> + + {asQuery ? ( + + } + getTableTitle={getTableTitle} + freeText={freeText} + setFreeText={setFreeText} + status={status} + setStatus={setStatus} + startTimeFrom={startTimeFrom} + setStartTimeFrom={setStartTimeFrom} + onStartFromChange={onStartFromChange} + startTimeTo={startTimeTo} + setStartTimeTo={setStartTimeTo} + onStartToChange={onStartToChange} + endTimeFrom={endTimeFrom} + setEndTimeFrom={setEndTimeFrom} + onEndFromChange={onEndFromChange} + endTimeTo={endTimeTo} + setEndTimeTo={setEndTimeTo} + onEndToChange={onEndToChange} + fromDisplayTime={fromDisplayTime} + setFromDisplayTime={setFromDisplayTime} + toDisplayTime={toDisplayTime} + setToDisplayTime={setToDisplayTime} + openDateSelect={openDateSelect} + setOpenDateSelect={setOpenDateSelect} + openStartDatePicker={openStartDatePicker} + setStartOpenDatePicker={setStartOpenDatePicker} + openEndDatePicker={openEndDatePicker} + setEndOpenDatePicker={setEndOpenDatePicker} + recentSearches={recentSearches} + /> + ) : ( + + } + getTableTitle={getTableTitle} + freeText={freeText} + setFreeText={setFreeText} + status={status} + setStatus={setStatus} + startTimeFrom={startTimeFrom} + setStartTimeFrom={setStartTimeFrom} + onStartFromChange={onStartFromChange} + startTimeTo={startTimeTo} + setStartTimeTo={setStartTimeTo} + onStartToChange={onStartToChange} + endTimeFrom={endTimeFrom} + setEndTimeFrom={setEndTimeFrom} + onEndFromChange={onEndFromChange} + endTimeTo={endTimeTo} + setEndTimeTo={setEndTimeTo} + onEndToChange={onEndToChange} + fromDisplayTime={fromDisplayTime} + setFromDisplayTime={setFromDisplayTime} + toDisplayTime={toDisplayTime} + setToDisplayTime={setToDisplayTime} + openDateSelect={openDateSelect} + setOpenDateSelect={setOpenDateSelect} + openStartDatePicker={openStartDatePicker} + setStartOpenDatePicker={setStartOpenDatePicker} + openEndDatePicker={openEndDatePicker} + setEndOpenDatePicker={setEndOpenDatePicker} + recentSearches={recentSearches} + /> + )} + + + ); +} diff --git a/ui-next/src/pages/executions/executionsStyles.ts b/ui-next/src/pages/executions/executionsStyles.ts new file mode 100644 index 0000000000..bc592ef51f --- /dev/null +++ b/ui-next/src/pages/executions/executionsStyles.ts @@ -0,0 +1,35 @@ +export default { + clickSearch: { + width: "100%", + padding: "30px", + paddingBottom: "0px", + display: "block", + textAlign: "center", + }, + paper: { + marginBottom: "30px", + }, + heading: { + marginBottom: "20px", + minHeight: "60px", + }, + controls: { + // padding: 15, + }, + popupIndicator: { + backgroundColor: "red", + }, + banner: { + marginBottom: "15px", + }, + actionBar: { + display: "flex", + alignItems: "center", + paddingRight: "10px", + "&>div, &>p": { + marginRight: "10px", + }, + width: "100%", + justifyContent: "space-between", + }, +}; diff --git a/ui-next/src/pages/executions/index.ts b/ui-next/src/pages/executions/index.ts new file mode 100644 index 0000000000..8071f8a5bf --- /dev/null +++ b/ui-next/src/pages/executions/index.ts @@ -0,0 +1,6 @@ +import WorkflowSearch from "./WorkflowSearch"; +import SchedulerExecutions from "./SchedulerExecutions"; + +export { SchedulerExecutions, WorkflowSearch }; + +export * from "./TaskSearch"; diff --git a/ui-next/src/pages/executions/workflowSearchComponents/AdvancedSearch.tsx b/ui-next/src/pages/executions/workflowSearchComponents/AdvancedSearch.tsx new file mode 100644 index 0000000000..5950d55eba --- /dev/null +++ b/ui-next/src/pages/executions/workflowSearchComponents/AdvancedSearch.tsx @@ -0,0 +1,589 @@ +import { Monaco } from "@monaco-editor/react"; +import { Box, Grid } from "@mui/material"; +import { Button, Paper } from "components"; +import { DEFAULT_ROWS_PER_PAGE } from "components/DataTable/DataTable"; +import MuiTypography from "components/MuiTypography"; +import StatusBadge from "components/StatusBadge"; +import { renderStatusTagChip } from "components/StatusTagChip"; +import { ConductorAutoComplete } from "components/v1"; +import { ConductorCodeBlockInput } from "components/v1/ConductorCodeBlockInput"; +import ConductorInput from "components/v1/ConductorInput"; +import SplitButton from "components/v1/ConductorSplitButton"; +import ResetIcon from "components/v1/icons/ResetIcon"; +import SearchIcon from "components/v1/icons/SearchIcon"; +import _isEmpty from "lodash/isEmpty"; +import { + ReactNode, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { useHotkeys } from "react-hotkeys-hook"; +import { Navigate } from "react-router"; +import { useQueryState } from "react-router-use-location-state"; +import { colors } from "theme/tokens/variables"; +import { Key } from "ts-key-enum"; +import { IObject } from "types/common"; +import { WorkflowExecutionStatus } from "types/Execution"; +import { TaskExecutionResult } from "types/TaskExecution"; +import { DoSearchProps } from "types/WorkflowExecution"; +import { dateToEpoch, useLocalStorage } from "utils"; +import { WORKFLOW_SEARCH_QUERY_SUGGESTIONS } from "utils/constants/common"; +import { ERROR_URL } from "utils/constants/route"; +import { useWorkflowNames, useWorkflowSearch } from "utils/query"; +import { getErrors } from "utils/utils"; +import { ApiSearchModalIntegration } from "../ApiSearchModalIntegration"; +import { DateControlComponent } from "../DateControlComponent"; +import ResultsTable from "../ResultsTable"; +import { ExampleSearchQuery } from "../SearchExampleQuery"; + +const DEFAULT_SORT = "startTime:DESC"; +const workflowStatuses = Object.values(WorkflowExecutionStatus); + +export interface AdvancedSearchProps { + doSearch: ({ + resultObj, + queryFT, + buildQuery, + setQueryFT, + refetch, + setPage, + setRecentTaskSearch, + }: DoSearchProps) => void; + SwitchComponent: ReactNode; + getTableTitle: (resultObj: TaskExecutionResult) => ReactNode; + freeText: string; + setFreeText: (val: string) => void; + status: string[]; + setStatus: (val: string[]) => void; + startTimeFrom: string; + setStartTimeFrom: (val: string) => void; + onStartFromChange: (val: string) => void; + startTimeTo: string; + setStartTimeTo: (val: string) => void; + onStartToChange: (val: string) => void; + endTimeFrom: string; + setEndTimeFrom: (val: string) => void; + onEndFromChange: (val: string) => void; + endTimeTo: string; + setEndTimeTo: (val: string) => void; + onEndToChange: (val: string) => void; + fromDisplayTime: string; + setFromDisplayTime: (val: string) => void; + toDisplayTime: string; + setToDisplayTime: (val: string) => void; + openDateSelect: boolean; + setOpenDateSelect: (val: boolean) => void; + openStartDatePicker: boolean; + setStartOpenDatePicker: (val: boolean) => void; + openEndDatePicker: boolean; + setEndOpenDatePicker: (val: boolean) => void; + recentSearches: { start: string; end: string }; +} + +export default function AdvancedSearch({ + doSearch, + SwitchComponent, + getTableTitle, + freeText, + setFreeText, + status, + setStatus, + startTimeFrom, + setStartTimeFrom, + onStartFromChange, + startTimeTo, + setStartTimeTo, + onStartToChange, + endTimeFrom, + setEndTimeFrom, + onEndFromChange, + endTimeTo, + setEndTimeTo, + onEndToChange, + fromDisplayTime, + setFromDisplayTime, + toDisplayTime, + setToDisplayTime, + openDateSelect, + setOpenDateSelect, + openStartDatePicker, + setStartOpenDatePicker, + openEndDatePicker, + setEndOpenDatePicker, + recentSearches, +}: AdvancedSearchProps) { + const disposeRef = useRef void)>(null); + const [queryText, setQueryText] = useQueryState("query", ""); + const [page, setPage] = useQueryState("page", 1); + const [rowsPerPage, setRowsPerPage] = useQueryState( + "rowsPerPage", + DEFAULT_ROWS_PER_PAGE, + ); + const [sort, setSort] = useQueryState("sort", DEFAULT_SORT); + const [showCodeDialog, setShowCodeDialog] = useQueryState("displayCode", ""); + + const [errorMessage, setErrorMessage] = useState(null); + + const [unauthorized, setUnauthorized] = useState<{ + message?: string; + error?: string; + } | null>(null); + + // For dropdown + const workflowNames: string[] = useWorkflowNames(); + + useEffect(() => { + return () => { + if (disposeRef.current) { + disposeRef.current(); + } + }; + }, []); + + // for tooltip flag in localstorage + const [tooltipFlags, setTooltipFlags] = useLocalStorage("tooltipFlags", {}); + const handleToolTipOnClose = () => { + if (tooltipFlags && !tooltipFlags.executionSearch) { + setTooltipFlags({ ...tooltipFlags, executionSearch: true }); + } + }; + + const currentTimeStamp = Date.now().toString(); + const last72HoursTimestamp = Date.now() - 72 * 60 * 60 * 1000; + + const buildQuery = useCallback(() => { + const clauses = []; + + if (!_isEmpty(status) && !queryText.includes("status")) { + clauses.push(`status IN (${status.join(",")})`); + } + + if (!queryText.includes("startTime")) { + if (!_isEmpty(startTimeFrom)) { + clauses.push(`startTime>${dateToEpoch(startTimeFrom)}`); + } + } + + if (!_isEmpty(startTimeTo)) { + clauses.push(`startTime<${dateToEpoch(startTimeTo)}`); + } + if (!_isEmpty(endTimeFrom)) { + clauses.push(`endTime>${dateToEpoch(endTimeFrom)}`); + } + if (!_isEmpty(endTimeTo)) { + clauses.push(`endTime<${dateToEpoch(endTimeTo)}`); + } + + if (!_isEmpty(queryText)) { + clauses.push(queryText); + } + + return { + query: clauses.join(" AND "), + freeText: _isEmpty(freeText) ? "*" : freeText, + }; + }, [ + freeText, + startTimeFrom, + startTimeTo, + endTimeFrom, + endTimeTo, + status, + queryText, + ]); + + const [queryFT, setQueryFT] = useState(buildQuery); + const { + data: resultObj, + error, + isFetching, + refetch, + } = useWorkflowSearch( + { + page, + rowsPerPage, + sort, + query: queryFT.query, + freeText: queryFT.freeText, + }, + {}, + { + onError: (error: any) => { + if (error) { + getErrors(error as Response).then((result) => { + if (result?.["workflowName"] === "must not be empty") { + setErrorMessage({ message: "Workflow name should not be empty" }); + } else { + setErrorMessage(result); + } + }); + } else { + setErrorMessage(null); + } + }, + staleTime: 0, + }, + ); + + // hotkeys to search execution + useHotkeys( + `${Key.Meta}+${Key.Enter}`, + () => + doSearch({ + resultObj, + queryFT, + buildQuery, + setQueryFT, + refetch, + setPage, + setRecentTaskSearch, + }), + { + enableOnFormTags: ["INPUT", "TEXTAREA", "SELECT"], + }, + ); + + // Must be called before any early returns to follow Rules of Hooks + const filterOn = useMemo(() => { + if (queryFT.query !== "" || queryFT.freeText !== "*") { + return true; + } else { + return false; + } + }, [queryFT]); + + const handlePage = (page: number) => { + setPage(page); + }; + + const handleSort = (changedColumn: string, direction: string) => { + const sortColumn = + changedColumn === "workflowType" ? "workflowName" : changedColumn; + const newSort = `${sortColumn}:${direction.toUpperCase()}`; + + // Only refetch if sort actually changed + if (sort !== newSort) { + setPage(1); + setSort(newSort); + refetch(); + } + }; + + // @ts-ignore + if (error?.status === 401) { + const errorResult = error; + const parseErrorResponse = async () => { + try { + // @ts-ignore + const json = await errorResult.clone().json(); + setUnauthorized(json); + } catch { + setUnauthorized(null); + } + }; + parseErrorResponse(); + } + + if (unauthorized) { + if (unauthorized.message) { + return ( + + ); + } + + return ; + } + + const handleError = (error: any) => { + setErrorMessage(error); + }; + const handleClearError = () => { + setErrorMessage(null); + }; + + const clearAllFields = () => { + setStatus([]); + setStartTimeFrom(""); + setStartTimeTo(""); + setEndTimeFrom(""); + setEndTimeTo(""); + setToDisplayTime(""); + setFromDisplayTime("Last 72 Hours"); + setFreeText(""); + setQueryText(""); + }; + + const handleReset = () => { + clearAllFields(); + setStartTimeFrom(last72HoursTimestamp.toString()); + setStartTimeTo(""); + const newQueryFT = { + query: `startTime>${last72HoursTimestamp.toString()} AND startTime<${currentTimeStamp}`, + freeText: "*", + }; + setQueryFT(newQueryFT); + }; + + const handleRowsPerPage = (rowsPerPage: number) => { + setPage(1); + setRowsPerPage(rowsPerPage); + }; + + const setRecentTaskSearch = () => { + if (startTimeFrom || startTimeTo || endTimeFrom || endTimeTo) { + localStorage.setItem( + "recentTaskSearch", + JSON.stringify({ + start: startTimeFrom || startTimeTo, + end: endTimeTo || endTimeFrom, + }), + ); + } + }; + + return ( + <> + + {SwitchComponent} + + {showCodeDialog && ( + setShowCodeDialog("")} + buildQueryOutput={{ + start: (page - 1) * rowsPerPage, + size: rowsPerPage, + sort, + freeText, + query: buildQuery().query, + }} + /> + )} + + + + Search workflow execution by query parameters. Then hit + ENTER, and now you can click SEARCH. + + + Sample: + + + + + ), + showInitial: !tooltipFlags.executionSearch, + initialTimeout: 2000, + onClose: handleToolTipOnClose, + }} + beforeMount={(monaco: Monaco) => { + if (disposeRef.current) { + disposeRef.current(); + disposeRef.current = null; + } + const disposable = + monaco.languages.registerCompletionItemProvider("sql", { + provideCompletionItems: () => { + const propertyKeys = [ + ...WORKFLOW_SEARCH_QUERY_SUGGESTIONS, + ...workflowStatuses, + ...workflowNames, + "workflowType", + ]; + // Provide suggestions for properties that start with the current text + const propertySuggestions = propertyKeys.map( + (property) => ({ + label: property, + kind: monaco.languages.CompletionItemKind.Value, + insertText: property, + }), + ); + // Merge custom suggestions with property suggestions + const suggestions = [...propertySuggestions]; + return { suggestions }; + }, + }); + // IMPORTANT: keep `dispose()` bound to its disposable context. + // Destructuring `dispose` can lose `this` and throw "Unbound disposable context". + disposeRef.current = () => disposable.dispose(); + }} + /> + + + + + + + + + + + + setStatus(val)} + value={status} + renderTags={renderStatusTagChip} + renderOption={(props, option) => ( + + + + )} + /> + + + + + + + + } + options={[ + { + label: "Show as code", + onClick: () => setShowCodeDialog("active"), + }, + ]} + primaryOnClick={() => + doSearch({ + resultObj, + queryFT, + buildQuery, + setQueryFT, + refetch, + setPage, + setRecentTaskSearch, + }) + } + > + Search + + + + +
    + + + + ); +} diff --git a/ui-next/src/pages/executions/workflowSearchComponents/BasicSearch.tsx b/ui-next/src/pages/executions/workflowSearchComponents/BasicSearch.tsx new file mode 100644 index 0000000000..266435b7b9 --- /dev/null +++ b/ui-next/src/pages/executions/workflowSearchComponents/BasicSearch.tsx @@ -0,0 +1,700 @@ +import { + Box, + FormControl, + Grid, + InputLabel, + useMediaQuery, +} from "@mui/material"; +import { Theme } from "@mui/material/styles"; +import { Button, Paper } from "components"; +import { DEFAULT_ROWS_PER_PAGE } from "components/DataTable/DataTable"; +import StatusBadge from "components/StatusBadge"; +import { renderStatusTagChip } from "components/StatusTagChip"; +import { ConductorAutoComplete } from "components/v1"; +import ConductorInput from "components/v1/ConductorInput"; +import SplitButton from "components/v1/ConductorSplitButton"; +import ResetIcon from "components/v1/icons/ResetIcon"; +import SearchIcon from "components/v1/icons/SearchIcon"; +import _isEmpty from "lodash/isEmpty"; +import { ReactNode, useCallback, useEffect, useMemo, useState } from "react"; +import { useHotkeys } from "react-hotkeys-hook"; +import { Navigate } from "react-router"; +import { useQueryState } from "react-router-use-location-state"; +import { Key } from "ts-key-enum"; +import { WorkflowExecutionStatus } from "types/Execution"; +import { TaskExecutionResult } from "types/TaskExecution"; +import { DoSearchProps } from "types/WorkflowExecution"; +import { IObject } from "types/common"; +import { dateToEpoch, useLocalStorage } from "utils"; +import { ERROR_URL } from "utils/constants/route"; +import { useAutoCompleteInputValidation } from "utils/hooks/useAutoCompleteInputValidation"; +import { useWorkflowNames, useWorkflowSearch } from "utils/query"; +import { getErrors } from "utils/utils"; +import { ApiSearchModalIntegration } from "../ApiSearchModalIntegration"; +import { DateControlComponent } from "../DateControlComponent"; +import ResultsTable from "../ResultsTable"; + +const DEFAULT_SORT = "startTime:DESC"; +const workflowStatuses = Object.values(WorkflowExecutionStatus); + +export interface BasicSearchProps { + doSearch: ({ + resultObj, + queryFT, + buildQuery, + setQueryFT, + refetch, + setPage, + setRecentTaskSearch, + }: DoSearchProps) => void; + SwitchComponent: ReactNode; + getTableTitle: (resultObj: TaskExecutionResult) => ReactNode; + freeText: string; + setFreeText: (val: string) => void; + status: string[]; + setStatus: (val: string[]) => void; + startTimeFrom: string; + setStartTimeFrom: (val: string) => void; + onStartFromChange: (val: string) => void; + startTimeTo: string; + setStartTimeTo: (val: string) => void; + onStartToChange: (val: string) => void; + endTimeFrom: string; + setEndTimeFrom: (val: string) => void; + onEndFromChange: (val: string) => void; + endTimeTo: string; + setEndTimeTo: (val: string) => void; + onEndToChange: (val: string) => void; + fromDisplayTime: string; + setFromDisplayTime: (val: string) => void; + toDisplayTime: string; + setToDisplayTime: (val: string) => void; + openDateSelect: boolean; + setOpenDateSelect: (val: boolean) => void; + openStartDatePicker: boolean; + setStartOpenDatePicker: (val: boolean) => void; + openEndDatePicker: boolean; + setEndOpenDatePicker: (val: boolean) => void; + recentSearches: { start: string; end: string }; +} + +export default function BasicSearch({ + doSearch, + SwitchComponent, + getTableTitle, + freeText, + setFreeText, + status, + setStatus, + startTimeFrom, + setStartTimeFrom, + onStartFromChange, + startTimeTo, + setStartTimeTo, + onStartToChange, + endTimeFrom, + setEndTimeFrom, + onEndFromChange, + endTimeTo, + setEndTimeTo, + onEndToChange, + fromDisplayTime, + setFromDisplayTime, + toDisplayTime, + setToDisplayTime, + openDateSelect, + setOpenDateSelect, + openStartDatePicker, + setStartOpenDatePicker, + openEndDatePicker, + setEndOpenDatePicker, + recentSearches, +}: BasicSearchProps) { + const [page, setPage] = useQueryState("page", 1); + const [workflowType, setWorkflowType] = useQueryState( + "workflowType", + [], + ); + const [workflowId, setWorkflowId] = useQueryState("workflowId", ""); + const [correlationIds, setCorrelationIds] = useQueryState( + "correlationIds", + [], + ); + const [idempotencyKey, setIdempotencyKey] = useQueryState( + "idempotencyKey", + [], + ); + + const [modifiedFrom, setModifiedFrom] = useQueryState("modifiedFrom", ""); + const [modifiedTo, setModifiedTo] = useQueryState("modifiedTo", ""); + + const [rowsPerPage, setRowsPerPage] = useQueryState( + "rowsPerPage", + DEFAULT_ROWS_PER_PAGE, + ); + const [sort, setSort] = useQueryState("sort", DEFAULT_SORT); + const [showCodeDialog, setShowCodeDialog] = useQueryState("displayCode", ""); + + const { + setValue: setCorrelationInputVal, + setFocused: setCorrelationFieldFocus, + hasError: correlationIdHasError, + } = useAutoCompleteInputValidation(); + + const { + setValue: setIdempotencyKeyInputVal, + setFocused: setIdempotencyKeyFieldFocus, + hasError: idempotencyKeyHasError, + } = useAutoCompleteInputValidation(); + + const isMobile = useMediaQuery((theme: Theme) => + theme.breakpoints.down("sm"), + ); + + // For dropdown + const workflowNames: string[] = useWorkflowNames(); + + // for tooltip flag in localstorage + const [tooltipFlags, setTooltipFlags] = useLocalStorage("tooltipFlags", {}); + const handleToolTipOnClose = () => { + if (tooltipFlags && !tooltipFlags.executionSearch) { + setTooltipFlags({ ...tooltipFlags, executionSearch: true }); + } + }; + + const handleRowsPerPage = (rowsPerPage: number) => { + setPage(1); + setRowsPerPage(rowsPerPage); + }; + + const clearAllFields = () => { + setWorkflowType([]); + setCorrelationIds([]); + setIdempotencyKey([]); + setWorkflowId(""); + setStatus([]); + setStartTimeFrom(""); + setStartTimeTo(""); + setFreeText(""); + setModifiedFrom(""); + setModifiedTo(""); + setEndTimeFrom(""); + setEndTimeTo(""); + setToDisplayTime("Now"); + setFromDisplayTime("Last 72 Hours"); + }; + + const currentTimeStamp = Date.now().toString(); + const last72HoursTimestamp = Date.now() - 72 * 60 * 60 * 1000; + + const handleReset = () => { + clearAllFields(); + setStartTimeFrom(String(last72HoursTimestamp)); + setStartTimeTo(""); + const newQueryFT = { + query: `startTime>${String( + last72HoursTimestamp, + )} AND startTime<${currentTimeStamp}`, + freeText: "*", + }; + setQueryFT(newQueryFT); + }; + + const [errorMessage, setErrorMessage] = useState(null); + + const [unauthorized, setUnauthorized] = useState<{ + message?: string; + error?: string; + } | null>(null); + + const buildQuery = useCallback(() => { + const clauses = []; + if (!_isEmpty(workflowType)) { + clauses.push(`workflowType IN (${workflowType.join(",")})`); + } + if (!_isEmpty(workflowId)) { + clauses.push(`workflowId='${workflowId}'`); + } + if (!_isEmpty(status)) { + clauses.push(`status IN (${status.join(",")})`); + } + if (!_isEmpty(startTimeFrom)) { + clauses.push(`startTime>${dateToEpoch(startTimeFrom)}`); + } + if (!_isEmpty(startTimeTo)) { + clauses.push(`startTime<${dateToEpoch(startTimeTo)}`); + } + if (!_isEmpty(endTimeFrom)) { + clauses.push(`endTime>${dateToEpoch(endTimeFrom)}`); + } + if (!_isEmpty(endTimeTo)) { + clauses.push(`endTime<${dateToEpoch(endTimeTo)}`); + } + + if (!_isEmpty(modifiedFrom)) { + clauses.push(`modifiedTime>${modifiedFrom}`); + } + if (!_isEmpty(modifiedTo)) { + clauses.push(`modifiedTime<${modifiedTo}`); + } + + if (!_isEmpty(correlationIds)) { + clauses.push(`correlationId IN (${correlationIds.join(",")})`); + } + + if (!_isEmpty(idempotencyKey)) { + clauses.push(`idempotencyKey IN (${idempotencyKey.join(",")})`); + } + + return { + query: clauses.join(" AND "), + freeText: _isEmpty(freeText) ? "*" : freeText, + }; + }, [ + freeText, + startTimeFrom, + startTimeTo, + status, + workflowId, + workflowType, + modifiedFrom, + modifiedTo, + correlationIds, + idempotencyKey, + endTimeFrom, + endTimeTo, + ]); + + const [queryFT, setQueryFT] = useState(buildQuery); + const { + data: resultObj, + error, + isFetching, + refetch, + } = useWorkflowSearch( + { + page, + rowsPerPage, + sort, + query: queryFT.query, + freeText: queryFT.freeText, + }, + {}, + { + onError: (error: any) => { + if (error) { + getErrors(error as Response).then((result) => { + if (result?.["workflowName"] === "must not be empty") { + setErrorMessage({ message: "Workflow name should not be empty" }); + } else { + setErrorMessage(result); + } + }); + } else { + setErrorMessage(null); + } + }, + }, + ); + + // hotkeys to search execution + useHotkeys( + `${Key.Meta}+${Key.Enter}`, + () => + doSearch({ + resultObj, + queryFT, + buildQuery, + setQueryFT, + refetch, + setPage, + setRecentTaskSearch, + }), + { + enableOnFormTags: ["INPUT", "TEXTAREA", "SELECT"], + }, + ); + + const handleSort = (changedColumn: string, direction: string) => { + const sortColumn = + changedColumn === "workflowType" ? "workflowName" : changedColumn; + const newSort = `${sortColumn}:${direction.toUpperCase()}`; + + // Only refetch if sort actually changed + if (sort !== newSort) { + setPage(1); + setSort(newSort); + refetch(); + } + }; + const handlePage = (page: number) => { + setPage(page); + }; + + const filterOn = useMemo(() => { + if (queryFT.query !== "" || queryFT.freeText !== "*") { + return true; + } else { + return false; + } + }, [queryFT]); + + const setRecentTaskSearch = () => { + if (startTimeFrom || startTimeTo || endTimeFrom || endTimeTo) { + localStorage.setItem( + "recentTaskSearch", + JSON.stringify({ + start: startTimeFrom || startTimeTo, + end: endTimeTo || endTimeFrom, + }), + ); + } + }; + + useEffect(() => { + if (!startTimeFrom) { + const currentTime = Date.now(); + const timestamp72HoursAgo = currentTime - 72 * 60 * 60 * 1000; + setStartTimeFrom(String(timestamp72HoursAgo)); + } + // eslint-disable-next-line + }, []); + + // @ts-ignore + if (error?.status === 401) { + const errorResult = error; + const parseErrorResponse = async () => { + try { + // @ts-ignore + const json = await errorResult.clone().json(); + setUnauthorized(json); + } catch { + setUnauthorized(null); + } + }; + parseErrorResponse(); + } + + if (unauthorized) { + if (unauthorized.message) { + return ( + + ); + } + + return ; + } + + const handleError = (error: any) => { + setErrorMessage(error); + }; + const handleClearError = () => { + setErrorMessage(null); + }; + + return ( + <> + + {SwitchComponent} + + {showCodeDialog && ( + setShowCodeDialog("")} + buildQueryOutput={{ + start: (page - 1) * rowsPerPage, + size: rowsPerPage, + sort, + freeText, + query: buildQuery().query, + }} + /> + )} + + + + a.toLowerCase().localeCompare(b.toLowerCase()), + )} + multiple + freeSolo + onChange={(__, val: string[]) => setWorkflowType(val)} + value={workflowType} + autoFocus + conductorInputProps={{ + tooltip: { + title: "Partial Name Search", + content: + "Search workflows by partial names with a wildcard * in your keyword. Then hit ENTER, and now you can click SEARCH. i.e. Workfl* or *orkfl*w", + placement: "top", + showInitial: !tooltipFlags.executionSearch ? true : false, + initialTimeout: 2000, + onClose: handleToolTipOnClose, + }, + autoFocus: true, + }} + /> + + + + + + { + setCorrelationInputVal(typingValue); + }} + onChange={(evt: any, val: string[]) => { + if (evt.key === "Backspace" || evt.key === "Enter") { + setCorrelationInputVal(""); + } + setCorrelationIds(val); + }} + onFocus={() => setCorrelationFieldFocus(true)} + onBlur={() => setCorrelationFieldFocus(false)} + value={correlationIds} + error={correlationIdHasError} + conductorInputProps={{ + tooltip: { + title: "Get Workflows by Correlation ID", + content: + "Search workflows by Correlation ID. This field has support for multiple values, so please remember to press 'Enter' for each value to apply the search.", + }, + error: correlationIdHasError, + }} + /> + + + { + setIdempotencyKeyInputVal(typingValue); + }} + onChange={(evt: any, val: string[]) => { + if (evt.key === "Backspace" || evt.key === "Enter") { + setIdempotencyKeyInputVal(""); + } + + setIdempotencyKey(val); + }} + onFocus={() => setIdempotencyKeyFieldFocus(true)} + onBlur={() => setIdempotencyKeyFieldFocus(false)} + value={idempotencyKey} + error={idempotencyKeyHasError} + conductorInputProps={{ + tooltip: { + title: "Get Workflows by Idempotency key", + content: + "Search workflows by Idempotency key. This field has support for multiple values, so please remember to press 'Enter' for each value to apply the search.", + }, + error: idempotencyKeyHasError, + }} + /> + + + setStatus(val)} + value={status} + renderTags={renderStatusTagChip} + renderOption={(props, option) => ( + + + + )} + /> + + + + + + + + + + + {!isMobile &&  } + + + + + + {!isMobile &&  } + + } + options={[ + { + label: "Show as code", + onClick: () => setShowCodeDialog("active"), + }, + ]} + primaryOnClick={() => + doSearch({ + resultObj, + queryFT, + buildQuery, + setQueryFT, + refetch, + setPage, + setRecentTaskSearch, + }) + } + > + Search + + + + + + + + + + ); +} diff --git a/ui-next/src/pages/kitchensink/DataTableDemo.jsx b/ui-next/src/pages/kitchensink/DataTableDemo.jsx new file mode 100644 index 0000000000..3f8cadccca --- /dev/null +++ b/ui-next/src/pages/kitchensink/DataTableDemo.jsx @@ -0,0 +1,19 @@ +import { DataTable } from "../../components"; +import data from "./sampleMovieData"; + +const DataTableDemo = () => { + const columns = [ + { id: "title", name: "title", label: "Title" }, + { id: "director", name: "director", label: "Director" }, + { id: "year", name: "year", label: "Year" }, + { id: "plot", name: "plot", label: "Plot", grow: 0.5 }, + ]; + + return ( + <> + + + ); +}; + +export default DataTableDemo; diff --git a/ui-next/src/pages/kitchensink/EnhancedTable.jsx b/ui-next/src/pages/kitchensink/EnhancedTable.jsx new file mode 100644 index 0000000000..a0ea260a18 --- /dev/null +++ b/ui-next/src/pages/kitchensink/EnhancedTable.jsx @@ -0,0 +1,349 @@ +import { + Box, + FormControlLabel, + Switch, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TablePagination, + TableRow, + TableSortLabel, + Toolbar, +} from "@mui/material"; +import { Heading, Paper, Text } from "components"; +import MuiCheckbox from "components/MuiCheckbox"; +import PropTypes from "prop-types"; +import { useState } from "react"; +import { INNER_HEADER_LEVEL } from "theme/tokens/globalConstants"; + +function createData(name, calories, fat, carbs, protein) { + return { name, calories, fat, carbs, protein }; +} + +const rows = [ + createData("Cupcake", 305, 3.7, 67, 4.3), + createData("Donut", 452, 25.0, 51, 4.9), + createData("Eclair", 262, 16.0, 24, 6.0), + createData("Frozen yoghurt", 159, 6.0, 24, 4.0), + createData("Gingerbread", 356, 16.0, 49, 3.9), + createData("Honeycomb", 408, 3.2, 87, 6.5), + createData("Ice cream sandwich", 237, 9.0, 37, 4.3), + createData("Jelly Bean", 375, 0.0, 94, 0.0), + createData("KitKat", 518, 26.0, 65, 7.0), + createData("Lollipop", 392, 0.2, 98, 0.0), + createData("Marshmallow", 318, 0, 81, 2.0), + createData("Nougat", 360, 19.0, 9, 37.0), + createData("Oreo", 437, 18.0, 63, 4.0), +]; + +function descendingComparator(a, b, orderBy) { + if (b[orderBy] < a[orderBy]) { + return -1; + } + if (b[orderBy] > a[orderBy]) { + return 1; + } + return 0; +} + +function getComparator(order, orderBy) { + return order === "desc" + ? (a, b) => descendingComparator(a, b, orderBy) + : (a, b) => -descendingComparator(a, b, orderBy); +} + +function stableSort(array, comparator) { + const stabilizedThis = array.map((el, index) => [el, index]); + stabilizedThis.sort((a, b) => { + const order = comparator(a[0], b[0]); + if (order !== 0) return order; + return a[1] - b[1]; + }); + return stabilizedThis.map((el) => el[0]); +} + +const headCells = [ + { + id: "name", + numeric: false, + disablePadding: true, + label: "Dessert (100g serving)", + }, + { id: "calories", numeric: true, disablePadding: false, label: "Calories" }, + { id: "fat", numeric: true, disablePadding: false, label: "Fat (g)" }, + { id: "carbs", numeric: true, disablePadding: false, label: "Carbs (g)" }, + { id: "protein", numeric: true, disablePadding: false, label: "Protein (g)" }, +]; + +function EnhancedTableHead(props) { + const { + customStyle, + onSelectAllClick, + order, + orderBy, + numSelected, + rowCount, + onRequestSort, + } = props; + const createSortHandler = (property) => (event) => { + onRequestSort(event, property); + }; + + return ( + + + + 0 && numSelected < rowCount} + checked={rowCount > 0 && numSelected === rowCount} + onChange={onSelectAllClick} + inputProps={{ "aria-label": "select all desserts" }} + /> + + {headCells.map((headCell) => ( + + + {headCell.label} + {orderBy === headCell.id ? ( + + {order === "desc" ? "sorted descending" : "sorted ascending"} + + ) : null} + + + ))} + + + ); +} + +EnhancedTableHead.propTypes = { + classes: PropTypes.object.isRequired, + numSelected: PropTypes.number.isRequired, + onRequestSort: PropTypes.func.isRequired, + onSelectAllClick: PropTypes.func.isRequired, + order: PropTypes.oneOf(["asc", "desc"]).isRequired, + orderBy: PropTypes.string.isRequired, + rowCount: PropTypes.number.isRequired, +}; + +const EnhancedTableToolbar = (props) => { + const { numSelected } = props; + + return ( + theme.spacing("2px"), + paddingRight: (theme) => theme.spacing("1px"), + color: (theme) => { + if (numSelected) { + return theme.palette.type === "light" + ? theme.palette.secondary.main + : theme.palette.text.primary; + } + + return ""; + }, + backgroundColor: (theme) => { + if (numSelected) { + return theme.palette.type === "light" + ? "" + : theme.palette.secondary.dark; + } + + return ""; + }, + }} + > + {numSelected > 0 ? {numSelected} selected : null} + + ); +}; + +EnhancedTableToolbar.propTypes = { + numSelected: PropTypes.number.isRequired, +}; + +const enhancedTableStyle = { + root: { + width: "100%", + }, + table: { + minWidth: "750px", + }, + visuallyHidden: { + border: 0, + clip: "rect(0 0 0 0)", + height: "1px", + margin: "-1px", + overflow: "hidden", + padding: 0, + position: "absolute", + top: "20px", + width: "1px", + }, +}; + +export default function EnhancedTable() { + const [order, setOrder] = useState("asc"); + const [orderBy, setOrderBy] = useState("calories"); + const [selected, setSelected] = useState([]); + const [page, setPage] = useState(0); + const [dense, setDense] = useState(false); + const [rowsPerPage, setRowsPerPage] = useState(5); + + const handleRequestSort = (event, property) => { + const isAsc = orderBy === property && order === "asc"; + setOrder(isAsc ? "desc" : "asc"); + setOrderBy(property); + }; + + const handleSelectAllClick = (event) => { + if (event.target.checked) { + const newSelecteds = rows.map((n) => n.name); + setSelected(newSelecteds); + return; + } + setSelected([]); + }; + + const handleClick = (event, name) => { + const selectedIndex = selected.indexOf(name); + let newSelected = []; + + if (selectedIndex === -1) { + newSelected = newSelected.concat(selected, name); + } else if (selectedIndex === 0) { + newSelected = newSelected.concat(selected.slice(1)); + } else if (selectedIndex === selected.length - 1) { + newSelected = newSelected.concat(selected.slice(0, -1)); + } else if (selectedIndex > 0) { + newSelected = newSelected.concat( + selected.slice(0, selectedIndex), + selected.slice(selectedIndex + 1), + ); + } + + setSelected(newSelected); + }; + + const handleChangePage = (event, newPage) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + + const handleChangeDense = (event) => { + setDense(event.target.checked); + }; + + const isSelected = (name) => selected.indexOf(name) !== -1; + + const emptyRows = + rowsPerPage - Math.min(rowsPerPage, rows.length - page * rowsPerPage); + + return ( + + theme.spacing(2), + }} + > + + Native MUI Table + + + + + + + {stableSort(rows, getComparator(order, orderBy)) + .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) + .map((row, index) => { + const isItemSelected = isSelected(row.name); + const labelId = `enhanced-table-checkbox-${index}`; + + return ( + handleClick(event, row.name)} + role="checkbox" + aria-checked={isItemSelected} + tabIndex={-1} + key={row.name} + selected={isItemSelected} + > + + + + + {row.name} + + {row.calories} + {row.fat} + {row.carbs} + {row.protein} + + ); + })} + {emptyRows > 0 && ( + + + + )} + +
    +
    + + } + label="Dense padding" + /> +
    +
    + ); +} diff --git a/ui-next/src/pages/kitchensink/Examples.jsx b/ui-next/src/pages/kitchensink/Examples.jsx new file mode 100644 index 0000000000..f1966e7b25 --- /dev/null +++ b/ui-next/src/pages/kitchensink/Examples.jsx @@ -0,0 +1,3 @@ +export default function Examples() { + return null; +} diff --git a/ui-next/src/pages/kitchensink/Gantt.jsx b/ui-next/src/pages/kitchensink/Gantt.jsx new file mode 100644 index 0000000000..fe39d994bd --- /dev/null +++ b/ui-next/src/pages/kitchensink/Gantt.jsx @@ -0,0 +1,87 @@ +import React, { Component } from "react"; +import Timeline from "react-vis-timeline"; +import { addMinutes, addMinutesToDate, startOfCurrentHour } from "utils/date"; +import { Paper } from "../../components"; + +function createItem(id, startTime) { + return { + id: id, + group: id, + content: "item " + id, + start: startTime, + end: addMinutes(startTime, 1), + }; +} + +const initialGroups = [], + initialItems = []; + +const now = startOfCurrentHour(); + +const itemCount = 20; +for (let i = 0; i < itemCount; i++) { + const start = addMinutesToDate(now, Math.random() * 200); + initialGroups.push({ id: i, content: "group " + i }); + initialItems.push(createItem(i, start)); +} + +export default class Gantt extends Component { + timelineRef = React.createRef(); + + constructor(props) { + super(props); + + this.state = { + selectedIds: [], + }; + } + + /* + onAddItem = () => { + var nextId = this.timelineRef.current.items.length + 1; + const group = Math.floor(Math.random() * groupCount); + this.timelineRef.current.items.add(createItem(nextId, group, names[group], moment())); + this.timelineRef.current.timeline.fit(); + }; + */ + + onFit = () => { + this.timelineRef.current.timeline.fit(); + }; + + render() { + return ( + +

    This example demonstrate using groups.

    + + +
    + +
    +
    +
    + ); + } + + clickHandler = () => { + const { group } = this.props; + const items = this.timelineRef.current.items.get(); + const selectedIds = items + .filter((item) => item.group === group) + .map((item) => item.id); + this.setState({ + selectedIds, + }); + }; +} diff --git a/ui-next/src/pages/kitchensink/KitchenSink.jsx b/ui-next/src/pages/kitchensink/KitchenSink.jsx new file mode 100644 index 0000000000..cfd5ac8324 --- /dev/null +++ b/ui-next/src/pages/kitchensink/KitchenSink.jsx @@ -0,0 +1,422 @@ +import { useState } from "react"; +import { + Box, + FormControl, + Grid, + InputLabel, + MenuItem, + Switch, +} from "@mui/material"; +import { Trash as DeleteIcon } from "@phosphor-icons/react"; +import { + ButtonGroup, + DropdownButton, + Heading, + IconButton, + Input, + NavLink, + Paper, + Select, + SplitButton, + Tab, + Tabs, + Text, +} from "components"; + +import EnhancedTable from "./EnhancedTable"; +import DataTableDemo from "./DataTableDemo"; +import { useAction } from "utils/query"; +import top100Films from "./sampleMovieData"; +import Dropdown from "components/Dropdown"; +import sharedStyles from "../styles"; +import { logger } from "utils/index"; +import Button from "components/MuiButton"; +import MuiCheckbox from "components/MuiCheckbox"; + +export default function KitchenSink() { + return ( + + + +

    This is a Hawkins-like theme based on vanilla Material-UI.

    +
    + + Gantt + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + ); +} + +const HeadingSection = () => { + return ( + + Heading Level Zero + Heading Level One + Heading Level Two + Heading Level Three + Heading Level Four + Heading Level Five + Text Level Zero + Text Level One + Text Level Two + +
    Default <div>
    +
    Default <p>
    +
    + ); +}; + +const TabsSection = () => { + const [tabIndex, setTabIndex] = useState(0); + + return ( + + + Tabs + + + Page Level + + + Full Width + + + + setTabIndex(0)} /> + setTabIndex(1)} /> + setTabIndex(2)} /> + setTabIndex(3)} /> + +
    Tab content {tabIndex}
    +
    + + + Fixed Width + + + + setTabIndex(0)} /> + setTabIndex(1)} /> + setTabIndex(2)} /> + setTabIndex(3)} /> + +
    Tab content {tabIndex}
    +
    + + + Contextual + + + + Full Width + + + + setTabIndex(0)} /> + setTabIndex(1)} /> + setTabIndex(2)} /> + setTabIndex(3)} /> + +
    Tab content {tabIndex}
    +
    + + Fixed Width + + + + + setTabIndex(0)} /> + setTabIndex(1)} /> + setTabIndex(2)} /> + setTabIndex(3)} /> + +
    Tab content {tabIndex}
    +
    +
    + ); +}; + +const Buttons = () => ( + + + Button + + + + + + + + + + + + + + + + + alert("you clicked 1"), + }, + { + label: "Squash and merge", + handler: () => alert("you clicked 2"), + }, + { + label: "Rebase and merge", + handler: () => alert("you clicked 3"), + }, + ]} + onPrimaryClick={() => alert("main button")} + > + Split Button + + + + alert("you clicked 1"), + }, + { + label: "Squash and merge", + handler: () => alert("you clicked 2"), + }, + { + label: "Rebase and merge", + handler: () => alert("you clicked 3"), + }, + ]} + > + Dropdown Button + + + + + + + + + + + + +); + +const Toggles = () => { + const [toggleChecked, setToggleChecked] = useState(false); + + return ( + + + Toggle + + setToggleChecked(!toggleChecked)} + color="primary" + /> + + ); +}; + +const Checkboxes = () => { + const [toggleChecked, setToggleChecked] = useState(false); + + return ( + + + Checkbox + + setToggleChecked(!toggleChecked)} + color="primary" + /> + + ); +}; + +const Inputs = () => ( + + + Input + + + + + + + + + + Input Label via FormControl/InputLabel + + + + + +); + +const Selects = () => { + const [value, setValue] = useState(10); + return ( + + + Select + + + + + + + + + option.title} + /> + + option.title} + /> + + option.title} + /> + + option.title} + defaultValue={[top100Films[13]]} + style={{ width: 500 }} + filterSelectedOptions + /> + + ); +}; + +const MutationTest = () => { + const postAction = useAction("/dummy/post", "post", { + onSuccess: (data) => logger.log("onsuccess", data), + onError: (err) => logger.log("onerror", err), + }); + + const putAction = useAction("/dummy/put", "put", { + onSuccess: (data) => logger.log("onsuccess", data), + onError: (err) => logger.log("onerror", err), + }); + + const deleteAction = useAction("/dummy/delete", "delete", { + onSuccess: (data) => logger.log("onsuccess", data), + onError: (err) => logger.log("onerror", err), + }); + + return ( + + + Mutations + + + + + + + + + + + + + + ); +}; diff --git a/ui-next/src/pages/kitchensink/ThemeSampler.jsx b/ui-next/src/pages/kitchensink/ThemeSampler.jsx new file mode 100644 index 0000000000..fb1d13a09b --- /dev/null +++ b/ui-next/src/pages/kitchensink/ThemeSampler.jsx @@ -0,0 +1,83 @@ +import { Box } from "@mui/material"; +import Button from "components/MuiButton"; +import MuiTypography from "components/MuiTypography"; + +export default function ThemeSampler() { + const variants = ["contained", "solid", "outlined"]; + const colors = ["primary", "secondary", "success", "warning", "error"]; + const sizes = ["small", "medium", "large"]; + + const variantColorsSizes = variants.reduce((acc, variant) => { + colors.forEach((color) => { + sizes.forEach((size) => { + acc.push({ variant, color, size }); + }); + }); + return acc; + }, []); + + return ( + + + + Buttons + + + + + + Default (no props) is: color = primary, variant = contained, size + = medium + + + {variantColorsSizes.map((variantColorSize) => { + return ( + + + + ); + })} + + {/* + + + color="secondary" + + + + + + color="secondary" + + */} + + + + ); +} diff --git a/ui-next/src/pages/kitchensink/sampleMovieData.js b/ui-next/src/pages/kitchensink/sampleMovieData.js new file mode 100644 index 0000000000..ad26468e9d --- /dev/null +++ b/ui-next/src/pages/kitchensink/sampleMovieData.js @@ -0,0 +1,1773 @@ +// Author https://github.com/yegor-sytnyk/movies-list + +export default [ + { + id: 1, + title: "Beetlejuice", + year: "1988", + runtime: "92", + genres: ["Comedy", "Fantasy"], + director: "Tim Burton", + actors: "Alec Baldwin, Geena Davis, Annie McEnroe, Maurice Page", + plot: 'A couple of recently deceased ghosts contract the services of a "bio-exorcist" in order to remove the obnoxious new owners of their house.', + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTUwODE3MDE0MV5BMl5BanBnXkFtZTgwNTk1MjI4MzE@._V1_SX300.jpg", + }, + { + id: 2, + title: "The Cotton Club", + year: "1984", + runtime: "127", + genres: ["Crime", "Drama", "Music"], + director: "Francis Ford Coppola", + actors: "Richard Gere, Gregory Hines, Diane Lane, Lonette McKee", + plot: "The Cotton Club was a famous night club in Harlem. The story follows the people that visited the club, those that ran it, and is peppered with the Jazz music that made it so famous.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTU5ODAyNzA4OV5BMl5BanBnXkFtZTcwNzYwNTIzNA@@._V1_SX300.jpg", + }, + { + id: 3, + title: "The Shawshank Redemption", + year: "1994", + runtime: "142", + genres: ["Crime", "Drama"], + director: "Frank Darabont", + actors: "Tim Robbins, Morgan Freeman, Bob Gunton, William Sadler", + plot: "Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BODU4MjU4NjIwNl5BMl5BanBnXkFtZTgwMDU2MjEyMDE@._V1_SX300.jpg", + }, + { + id: 4, + title: "Crocodile Dundee", + year: "1986", + runtime: "97", + genres: ["Adventure", "Comedy"], + director: "Peter Faiman", + actors: "Paul Hogan, Linda Kozlowski, John Meillon, David Gulpilil", + plot: "An American reporter goes to the Australian outback to meet an eccentric crocodile poacher and invites him to New York City.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTg0MTU1MTg4NF5BMl5BanBnXkFtZTgwMDgzNzYxMTE@._V1_SX300.jpg", + }, + { + id: 5, + title: "Valkyrie", + year: "2008", + runtime: "121", + genres: ["Drama", "History", "Thriller"], + director: "Bryan Singer", + actors: "Tom Cruise, Kenneth Branagh, Bill Nighy, Tom Wilkinson", + plot: "A dramatization of the 20 July assassination and political coup plot by desperate renegade German Army officers against Hitler during World War II.", + posterUrl: + "http://ia.media-imdb.com/images/M/MV5BMTg3Njc2ODEyN15BMl5BanBnXkFtZTcwNTAwMzc3NA@@._V1_SX300.jpg", + }, + { + id: 6, + title: "Ratatouille", + year: "2007", + runtime: "111", + genres: ["Animation", "Comedy", "Family"], + director: "Brad Bird, Jan Pinkava", + actors: "Patton Oswalt, Ian Holm, Lou Romano, Brian Dennehy", + plot: "A rat who can cook makes an unusual alliance with a young kitchen worker at a famous restaurant.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTMzODU0NTkxMF5BMl5BanBnXkFtZTcwMjQ4MzMzMw@@._V1_SX300.jpg", + }, + { + id: 7, + title: "City of God", + year: "2002", + runtime: "130", + genres: ["Crime", "Drama"], + director: "Fernando Meirelles, Kátia Lund", + actors: + "Alexandre Rodrigues, Leandro Firmino, Phellipe Haagensen, Douglas Silva", + plot: "Two boys growing up in a violent neighborhood of Rio de Janeiro take different paths: one becomes a photographer, the other a drug dealer.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMjA4ODQ3ODkzNV5BMl5BanBnXkFtZTYwOTc4NDI3._V1_SX300.jpg", + }, + { + id: 8, + title: "Memento", + year: "2000", + runtime: "113", + genres: ["Mystery", "Thriller"], + director: "Christopher Nolan", + actors: "Guy Pearce, Carrie-Anne Moss, Joe Pantoliano, Mark Boone Junior", + plot: "A man juggles searching for his wife's murderer and keeping his short-term memory loss from being an obstacle.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BNThiYjM3MzktMDg3Yy00ZWQ3LTk3YWEtN2M0YmNmNWEwYTE3XkEyXkFqcGdeQXVyMTQxNzMzNDI@._V1_SX300.jpg", + }, + { + id: 9, + title: "The Intouchables", + year: "2011", + runtime: "112", + genres: ["Biography", "Comedy", "Drama"], + director: "Olivier Nakache, Eric Toledano", + actors: "François Cluzet, Omar Sy, Anne Le Ny, Audrey Fleurot", + plot: "After he becomes a quadriplegic from a paragliding accident, an aristocrat hires a young man from the projects to be his caregiver.", + posterUrl: + "http://ia.media-imdb.com/images/M/MV5BMTYxNDA3MDQwNl5BMl5BanBnXkFtZTcwNTU4Mzc1Nw@@._V1_SX300.jpg", + }, + { + id: 10, + title: "Stardust", + year: "2007", + runtime: "127", + genres: ["Adventure", "Family", "Fantasy"], + director: "Matthew Vaughn", + actors: "Ian McKellen, Bimbo Hart, Alastair MacIntosh, David Kelly", + plot: "In a countryside town bordering on a magical land, a young man makes a promise to his beloved that he'll retrieve a fallen star by venturing into the magical realm.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMjkyMTE1OTYwNF5BMl5BanBnXkFtZTcwMDIxODYzMw@@._V1_SX300.jpg", + }, + { + id: 11, + title: "Apocalypto", + year: "2006", + runtime: "139", + genres: ["Action", "Adventure", "Drama"], + director: "Mel Gibson", + actors: + "Rudy Youngblood, Dalia Hernández, Jonathan Brewer, Morris Birdyellowhead", + plot: "As the Mayan kingdom faces its decline, the rulers insist the key to prosperity is to build more temples and offer human sacrifices. Jaguar Paw, a young man captured for sacrifice, flees to avoid his fate.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BNTM1NjYyNTY5OV5BMl5BanBnXkFtZTcwMjgwNTMzMQ@@._V1_SX300.jpg", + }, + { + id: 12, + title: "Taxi Driver", + year: "1976", + runtime: "113", + genres: ["Crime", "Drama"], + director: "Martin Scorsese", + actors: "Diahnne Abbott, Frank Adu, Victor Argo, Gino Ardito", + plot: "A mentally unstable Vietnam War veteran works as a night-time taxi driver in New York City where the perceived decadence and sleaze feeds his urge for violent action, attempting to save a preadolescent prostitute in the process.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BNGQxNDgzZWQtZTNjNi00M2RkLWExZmEtNmE1NjEyZDEwMzA5XkEyXkFqcGdeQXVyMTQxNzMzNDI@._V1_SX300.jpg", + }, + { + id: 13, + title: "No Country for Old Men", + year: "2007", + runtime: "122", + genres: ["Crime", "Drama", "Thriller"], + director: "Ethan Coen, Joel Coen", + actors: "Tommy Lee Jones, Javier Bardem, Josh Brolin, Woody Harrelson", + plot: "Violence and mayhem ensue after a hunter stumbles upon a drug deal gone wrong and more than two million dollars in cash near the Rio Grande.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMjA5Njk3MjM4OV5BMl5BanBnXkFtZTcwMTc5MTE1MQ@@._V1_SX300.jpg", + }, + { + id: 14, + title: "Planet 51", + year: "2009", + runtime: "91", + genres: ["Animation", "Adventure", "Comedy"], + director: "Jorge Blanco, Javier Abad, Marcos Martínez", + actors: "Jessica Biel, John Cleese, Gary Oldman, Dwayne Johnson", + plot: "An alien civilization is invaded by Astronaut Chuck Baker, who believes that the planet was uninhabited. Wanted by the military, Baker must get back to his ship before it goes into orbit without him.", + posterUrl: + "http://ia.media-imdb.com/images/M/MV5BMTUyOTAyNTA5Ml5BMl5BanBnXkFtZTcwODU2OTM0Mg@@._V1_SX300.jpg", + }, + { + id: 15, + title: "Looper", + year: "2012", + runtime: "119", + genres: ["Action", "Crime", "Drama"], + director: "Rian Johnson", + actors: "Joseph Gordon-Levitt, Bruce Willis, Emily Blunt, Paul Dano", + plot: "In 2074, when the mob wants to get rid of someone, the target is sent into the past, where a hired gun awaits - someone like Joe - who one day learns the mob wants to 'close the loop' by sending back Joe's future self for assassination.", + posterUrl: + "http://ia.media-imdb.com/images/M/MV5BMTY3NTY0MjEwNV5BMl5BanBnXkFtZTcwNTE3NDA1OA@@._V1_SX300.jpg", + }, + { + id: 16, + title: "Corpse Bride", + year: "2005", + runtime: "77", + genres: ["Animation", "Drama", "Family"], + director: "Tim Burton, Mike Johnson", + actors: "Johnny Depp, Helena Bonham Carter, Emily Watson, Tracey Ullman", + plot: "When a shy groom practices his wedding vows in the inadvertent presence of a deceased young woman, she rises from the grave assuming he has married her.", + posterUrl: + "http://ia.media-imdb.com/images/M/MV5BMTk1MTY1NjU4MF5BMl5BanBnXkFtZTcwNjIzMTEzMw@@._V1_SX300.jpg", + }, + { + id: 17, + title: "The Third Man", + year: "1949", + runtime: "93", + genres: ["Film-Noir", "Mystery", "Thriller"], + director: "Carol Reed", + actors: "Joseph Cotten, Alida Valli, Orson Welles, Trevor Howard", + plot: "Pulp novelist Holly Martins travels to shadowy, postwar Vienna, only to find himself investigating the mysterious death of an old friend, Harry Lime.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMjMwNzMzMTQ0Ml5BMl5BanBnXkFtZTgwNjExMzUwNjE@._V1_SX300.jpg", + }, + { + id: 18, + title: "The Beach", + year: "2000", + runtime: "119", + genres: ["Adventure", "Drama", "Romance"], + director: "Danny Boyle", + actors: + "Leonardo DiCaprio, Daniel York, Patcharawan Patarakijjanon, Virginie Ledoyen", + plot: "Twenty-something Richard travels to Thailand and finds himself in possession of a strange map. Rumours state that it leads to a solitary beach paradise, a tropical bliss - excited and intrigued, he sets out to find it.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BN2ViYTFiZmUtOTIxZi00YzIxLWEyMzUtYjQwZGNjMjNhY2IwXkEyXkFqcGdeQXVyNDk3NzU2MTQ@._V1_SX300.jpg", + }, + { + id: 19, + title: "Scarface", + year: "1983", + runtime: "170", + genres: ["Crime", "Drama"], + director: "Brian De Palma", + actors: + "Al Pacino, Steven Bauer, Michelle Pfeiffer, Mary Elizabeth Mastrantonio", + plot: "In Miami in 1980, a determined Cuban immigrant takes over a drug cartel and succumbs to greed.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMjAzOTM4MzEwNl5BMl5BanBnXkFtZTgwMzU1OTc1MDE@._V1_SX300.jpg", + }, + { + id: 20, + title: "Sid and Nancy", + year: "1986", + runtime: "112", + genres: ["Biography", "Drama", "Music"], + director: "Alex Cox", + actors: "Gary Oldman, Chloe Webb, David Hayman, Debby Bishop", + plot: "Morbid biographical story of Sid Vicious, bassist with British punk group the Sex Pistols, and his girlfriend Nancy Spungen. When the Sex Pistols break up after their fateful US tour, ...", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMjExNjA5NzY4M15BMl5BanBnXkFtZTcwNjQ2NzI5NA@@._V1_SX300.jpg", + }, + { + id: 21, + title: "Black Swan", + year: "2010", + runtime: "108", + genres: ["Drama", "Thriller"], + director: "Darren Aronofsky", + actors: "Natalie Portman, Mila Kunis, Vincent Cassel, Barbara Hershey", + plot: 'A committed dancer wins the lead role in a production of Tchaikovsky\'s "Swan Lake" only to find herself struggling to maintain her sanity.', + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BNzY2NzI4OTE5MF5BMl5BanBnXkFtZTcwMjMyNDY4Mw@@._V1_SX300.jpg", + }, + { + id: 22, + title: "Inception", + year: "2010", + runtime: "148", + genres: ["Action", "Adventure", "Sci-Fi"], + director: "Christopher Nolan", + actors: "Leonardo DiCaprio, Joseph Gordon-Levitt, Ellen Page, Tom Hardy", + plot: "A thief, who steals corporate secrets through use of dream-sharing technology, is given the inverse task of planting an idea into the mind of a CEO.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMjAxMzY3NjcxNF5BMl5BanBnXkFtZTcwNTI5OTM0Mw@@._V1_SX300.jpg", + }, + { + id: 23, + title: "The Deer Hunter", + year: "1978", + runtime: "183", + genres: ["Drama", "War"], + director: "Michael Cimino", + actors: "Robert De Niro, John Cazale, John Savage, Christopher Walken", + plot: "An in-depth examination of the ways in which the U.S. Vietnam War impacts and disrupts the lives of people in a small industrial town in Pennsylvania.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTYzYmRmZTQtYjk2NS00MDdlLTkxMDAtMTE2YTM2ZmNlMTBkXkEyXkFqcGdeQXVyNjU0OTQ0OTY@._V1_SX300.jpg", + }, + { + id: 24, + title: "Chasing Amy", + year: "1997", + runtime: "113", + genres: ["Comedy", "Drama", "Romance"], + director: "Kevin Smith", + actors: "Ethan Suplee, Ben Affleck, Scott Mosier, Jason Lee", + plot: "Holden and Banky are comic book artists. Everything's going good for them until they meet Alyssa, also a comic book artist. Holden falls for her, but his hopes are crushed when he finds out she's gay.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BZDM3MTg2MGUtZDM0MC00NzMwLWE5NjItOWFjNjA2M2I4YzgxXkEyXkFqcGdeQXVyMTQxNzMzNDI@._V1_SX300.jpg", + }, + { + id: 25, + title: "Django Unchained", + year: "2012", + runtime: "165", + genres: ["Drama", "Western"], + director: "Quentin Tarantino", + actors: "Jamie Foxx, Christoph Waltz, Leonardo DiCaprio, Kerry Washington", + plot: "With the help of a German bounty hunter, a freed slave sets out to rescue his wife from a brutal Mississippi plantation owner.", + posterUrl: + "http://ia.media-imdb.com/images/M/MV5BMjIyNTQ5NjQ1OV5BMl5BanBnXkFtZTcwODg1MDU4OA@@._V1_SX300.jpg", + }, + { + id: 26, + title: "The Silence of the Lambs", + year: "1991", + runtime: "118", + genres: ["Crime", "Drama", "Thriller"], + director: "Jonathan Demme", + actors: + "Jodie Foster, Lawrence A. Bonney, Kasi Lemmons, Lawrence T. Wrentz", + plot: "A young F.B.I. cadet must confide in an incarcerated and manipulative killer to receive his help on catching another serial killer who skins his victims.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTQ2NzkzMDI4OF5BMl5BanBnXkFtZTcwMDA0NzE1NA@@._V1_SX300.jpg", + }, + { + id: 27, + title: "American Beauty", + year: "1999", + runtime: "122", + genres: ["Drama", "Romance"], + director: "Sam Mendes", + actors: "Kevin Spacey, Annette Bening, Thora Birch, Wes Bentley", + plot: "A sexually frustrated suburban father has a mid-life crisis after becoming infatuated with his daughter's best friend.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMjM4NTI5NzYyNV5BMl5BanBnXkFtZTgwNTkxNTYxMTE@._V1_SX300.jpg", + }, + { + id: 28, + title: "Snatch", + year: "2000", + runtime: "102", + genres: ["Comedy", "Crime"], + director: "Guy Ritchie", + actors: "Benicio Del Toro, Dennis Farina, Vinnie Jones, Brad Pitt", + plot: "Unscrupulous boxing promoters, violent bookmakers, a Russian gangster, incompetent amateur robbers, and supposedly Jewish jewelers fight to track down a priceless stolen diamond.", + posterUrl: + "http://ia.media-imdb.com/images/M/MV5BMTA2NDYxOGYtYjU1Mi00Y2QzLTgxMTQtMWI1MGI0ZGQ5MmU4XkEyXkFqcGdeQXVyNDk3NzU2MTQ@._V1_SX300.jpg", + }, + { + id: 29, + title: "Midnight Express", + year: "1978", + runtime: "121", + genres: ["Crime", "Drama", "Thriller"], + director: "Alan Parker", + actors: "Brad Davis, Irene Miracle, Bo Hopkins, Paolo Bonacelli", + plot: "Billy Hayes, an American college student, is caught smuggling drugs out of Turkey and thrown into prison.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTQyMDA5MzkyOF5BMl5BanBnXkFtZTgwOTYwNTcxMTE@._V1_SX300.jpg", + }, + { + id: 30, + title: "Pulp Fiction", + year: "1994", + runtime: "154", + genres: ["Crime", "Drama"], + director: "Quentin Tarantino", + actors: "Tim Roth, Amanda Plummer, Laura Lovelace, John Travolta", + plot: "The lives of two mob hit men, a boxer, a gangster's wife, and a pair of diner bandits intertwine in four tales of violence and redemption.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTkxMTA5OTAzMl5BMl5BanBnXkFtZTgwNjA5MDc3NjE@._V1_SX300.jpg", + }, + { + id: 31, + title: "Lock, Stock and Two Smoking Barrels", + year: "1998", + runtime: "107", + genres: ["Comedy", "Crime"], + director: "Guy Ritchie", + actors: "Jason Flemyng, Dexter Fletcher, Nick Moran, Jason Statham", + plot: "A botched card game in London triggers four friends, thugs, weed-growers, hard gangsters, loan sharks and debt collectors to collide with each other in a series of unexpected events, all for the sake of weed, cash and two antique shotguns.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTAyN2JmZmEtNjAyMy00NzYwLThmY2MtYWQ3OGNhNjExMmM4XkEyXkFqcGdeQXVyNDk3NzU2MTQ@._V1_SX300.jpg", + }, + { + id: 32, + title: "Lucky Number Slevin", + year: "2006", + runtime: "110", + genres: ["Crime", "Drama", "Mystery"], + director: "Paul McGuigan", + actors: "Josh Hartnett, Bruce Willis, Lucy Liu, Morgan Freeman", + plot: "A case of mistaken identity lands Slevin into the middle of a war being plotted by two of the city's most rival crime bosses: The Rabbi and The Boss. Slevin is under constant surveillance by relentless Detective Brikowski as well as the infamous assassin Goodkat and finds himself having to hatch his own ingenious plot to get them before they get him.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMzc1OTEwMTk4OF5BMl5BanBnXkFtZTcwMTEzMDQzMQ@@._V1_SX300.jpg", + }, + { + id: 33, + title: "Rear Window", + year: "1954", + runtime: "112", + genres: ["Mystery", "Thriller"], + director: "Alfred Hitchcock", + actors: "James Stewart, Grace Kelly, Wendell Corey, Thelma Ritter", + plot: "A wheelchair-bound photographer spies on his neighbours from his apartment window and becomes convinced one of them has committed murder.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BNGUxYWM3M2MtMGM3Mi00ZmRiLWE0NGQtZjE5ODI2OTJhNTU0XkEyXkFqcGdeQXVyMTQxNzMzNDI@._V1_SX300.jpg", + }, + { + id: 34, + title: "Pan's Labyrinth", + year: "2006", + runtime: "118", + genres: ["Drama", "Fantasy", "War"], + director: "Guillermo del Toro", + actors: "Ivana Baquero, Sergi López, Maribel Verdú, Doug Jones", + plot: "In the falangist Spain of 1944, the bookish young stepdaughter of a sadistic army officer escapes into an eerie but captivating fantasy world.", + posterUrl: "", + }, + { + id: 35, + title: "Shutter Island", + year: "2010", + runtime: "138", + genres: ["Mystery", "Thriller"], + director: "Martin Scorsese", + actors: "Leonardo DiCaprio, Mark Ruffalo, Ben Kingsley, Max von Sydow", + plot: "In 1954, a U.S. marshal investigates the disappearance of a murderess who escaped from a hospital for the criminally insane.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTMxMTIyNzMxMV5BMl5BanBnXkFtZTcwOTc4OTI3Mg@@._V1_SX300.jpg", + }, + { + id: 36, + title: "Reservoir Dogs", + year: "1992", + runtime: "99", + genres: ["Crime", "Drama", "Thriller"], + director: "Quentin Tarantino", + actors: "Harvey Keitel, Tim Roth, Michael Madsen, Chris Penn", + plot: "After a simple jewelry heist goes terribly wrong, the surviving criminals begin to suspect that one of them is a police informant.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BNjE5ZDJiZTQtOGE2YS00ZTc5LTk0OGUtOTg2NjdjZmVlYzE2XkEyXkFqcGdeQXVyMzM4MjM0Nzg@._V1_SX300.jpg", + }, + { + id: 37, + title: "The Shining", + year: "1980", + runtime: "146", + genres: ["Drama", "Horror"], + director: "Stanley Kubrick", + actors: "Jack Nicholson, Shelley Duvall, Danny Lloyd, Scatman Crothers", + plot: "A family heads to an isolated hotel for the winter where an evil and spiritual presence influences the father into violence, while his psychic son sees horrific forebodings from the past and of the future.", + posterUrl: + "http://ia.media-imdb.com/images/M/MV5BODMxMjE3NTA4Ml5BMl5BanBnXkFtZTgwNDc0NTIxMDE@._V1_SX300.jpg", + }, + { + id: 38, + title: "Midnight in Paris", + year: "2011", + runtime: "94", + genres: ["Comedy", "Fantasy", "Romance"], + director: "Woody Allen", + actors: "Owen Wilson, Rachel McAdams, Kurt Fuller, Mimi Kennedy", + plot: "While on a trip to Paris with his fiancée's family, a nostalgic screenwriter finds himself mysteriously going back to the 1920s everyday at midnight.", + posterUrl: + "http://ia.media-imdb.com/images/M/MV5BMTM4NjY1MDQwMl5BMl5BanBnXkFtZTcwNTI3Njg3NA@@._V1_SX300.jpg", + }, + { + id: 39, + title: "Les Misérables", + year: "2012", + runtime: "158", + genres: ["Drama", "Musical", "Romance"], + director: "Tom Hooper", + actors: "Hugh Jackman, Russell Crowe, Anne Hathaway, Amanda Seyfried", + plot: "In 19th-century France, Jean Valjean, who for decades has been hunted by the ruthless policeman Javert after breaking parole, agrees to care for a factory worker's daughter. The decision changes their lives forever.", + posterUrl: + "http://ia.media-imdb.com/images/M/MV5BMTQ4NDI3NDg4M15BMl5BanBnXkFtZTcwMjY5OTI1OA@@._V1_SX300.jpg", + }, + { + id: 40, + title: "L.A. Confidential", + year: "1997", + runtime: "138", + genres: ["Crime", "Drama", "Mystery"], + director: "Curtis Hanson", + actors: "Kevin Spacey, Russell Crowe, Guy Pearce, James Cromwell", + plot: "As corruption grows in 1950s LA, three policemen - one strait-laced, one brutal, and one sleazy - investigate a series of murders with their own brand of justice.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BNWEwNDhhNWUtYWMzNi00ZTNhLWFiZDAtMjBjZmJhMTU0ZTY2XkEyXkFqcGdeQXVyNDk3NzU2MTQ@._V1_SX300.jpg", + }, + { + id: 41, + title: "Moneyball", + year: "2011", + runtime: "133", + genres: ["Biography", "Drama", "Sport"], + director: "Bennett Miller", + actors: "Brad Pitt, Jonah Hill, Philip Seymour Hoffman, Robin Wright", + plot: "Oakland A's general manager Billy Beane's successful attempt to assemble a baseball team on a lean budget by employing computer-generated analysis to acquire new players.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMjAxOTU3Mzc1M15BMl5BanBnXkFtZTcwMzk1ODUzNg@@._V1_SX300.jpg", + }, + { + id: 42, + title: "The Hangover", + year: "2009", + runtime: "100", + genres: ["Comedy"], + director: "Todd Phillips", + actors: "Bradley Cooper, Ed Helms, Zach Galifianakis, Justin Bartha", + plot: "Three buddies wake up from a bachelor party in Las Vegas, with no memory of the previous night and the bachelor missing. They make their way around the city in order to find their friend before his wedding.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTU1MDA1MTYwMF5BMl5BanBnXkFtZTcwMDcxMzA1Mg@@._V1_SX300.jpg", + }, + { + id: 43, + title: "The Great Beauty", + year: "2013", + runtime: "141", + genres: ["Drama"], + director: "Paolo Sorrentino", + actors: "Toni Servillo, Carlo Verdone, Sabrina Ferilli, Carlo Buccirosso", + plot: "Jep Gambardella has seduced his way through the lavish nightlife of Rome for decades, but after his 65th birthday and a shock from the past, Jep looks past the nightclubs and parties to find a timeless landscape of absurd, exquisite beauty.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTQ0ODg1OTQ2Nl5BMl5BanBnXkFtZTgwNTc2MDY1MDE@._V1_SX300.jpg", + }, + { + id: 44, + title: "Gran Torino", + year: "2008", + runtime: "116", + genres: ["Drama"], + director: "Clint Eastwood", + actors: "Clint Eastwood, Christopher Carley, Bee Vang, Ahney Her", + plot: "Disgruntled Korean War veteran Walt Kowalski sets out to reform his neighbor, a Hmong teenager who tried to steal Kowalski's prized possession: a 1972 Gran Torino.", + posterUrl: + "http://ia.media-imdb.com/images/M/MV5BMTQyMTczMTAxMl5BMl5BanBnXkFtZTcwOTc1ODE0Mg@@._V1_SX300.jpg", + }, + { + id: 45, + title: "Mary and Max", + year: "2009", + runtime: "92", + genres: ["Animation", "Comedy", "Drama"], + director: "Adam Elliot", + actors: "Toni Collette, Philip Seymour Hoffman, Barry Humphries, Eric Bana", + plot: "A tale of friendship between two unlikely pen pals: Mary, a lonely, eight-year-old girl living in the suburbs of Melbourne, and Max, a forty-four-year old, severely obese man living in New York.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTQ1NDIyNTA1Nl5BMl5BanBnXkFtZTcwMjc2Njk3OA@@._V1_SX300.jpg", + }, + { + id: 46, + title: "Flight", + year: "2012", + runtime: "138", + genres: ["Drama", "Thriller"], + director: "Robert Zemeckis", + actors: + "Nadine Velazquez, Denzel Washington, Carter Cabassa, Adam C. Edwards", + plot: "An airline pilot saves almost all his passengers on his malfunctioning airliner which eventually crashed, but an investigation into the accident reveals something troubling.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTUxMjI1OTMxNl5BMl5BanBnXkFtZTcwNjc3NTY1OA@@._V1_SX300.jpg", + }, + { + id: 47, + title: "One Flew Over the Cuckoo's Nest", + year: "1975", + runtime: "133", + genres: ["Drama"], + director: "Milos Forman", + actors: "Michael Berryman, Peter Brocco, Dean R. Brooks, Alonzo Brown", + plot: "A criminal pleads insanity after getting into trouble again and once in the mental institution rebels against the oppressive nurse and rallies up the scared patients.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BYmJkODkwOTItZThjZC00MTE0LWIxNzQtYTM3MmQwMGI1OWFiXkEyXkFqcGdeQXVyNjUwNzk3NDc@._V1_SX300.jpg", + }, + { + id: 48, + title: "Requiem for a Dream", + year: "2000", + runtime: "102", + genres: ["Drama"], + director: "Darren Aronofsky", + actors: "Ellen Burstyn, Jared Leto, Jennifer Connelly, Marlon Wayans", + plot: "The drug-induced utopias of four Coney Island people are shattered when their addictions run deep.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTkzODMzODYwOF5BMl5BanBnXkFtZTcwODM2NjA2NQ@@._V1_SX300.jpg", + }, + { + id: 49, + title: "The Truman Show", + year: "1998", + runtime: "103", + genres: ["Comedy", "Drama", "Sci-Fi"], + director: "Peter Weir", + actors: "Jim Carrey, Laura Linney, Noah Emmerich, Natascha McElhone", + plot: "An insurance salesman/adjuster discovers his entire life is actually a television show.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMDIzODcyY2EtMmY2MC00ZWVlLTgwMzAtMjQwOWUyNmJjNTYyXkEyXkFqcGdeQXVyNDk3NzU2MTQ@._V1_SX300.jpg", + }, + { + id: 50, + title: "The Artist", + year: "2011", + runtime: "100", + genres: ["Comedy", "Drama", "Romance"], + director: "Michel Hazanavicius", + actors: "Jean Dujardin, Bérénice Bejo, John Goodman, James Cromwell", + plot: "A silent movie star meets a young dancer, but the arrival of talking pictures sends their careers in opposite directions.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMzk0NzQxMTM0OV5BMl5BanBnXkFtZTcwMzU4MDYyNQ@@._V1_SX300.jpg", + }, + { + id: 51, + title: "Forrest Gump", + year: "1994", + runtime: "142", + genres: ["Comedy", "Drama"], + director: "Robert Zemeckis", + actors: + "Tom Hanks, Rebecca Williams, Sally Field, Michael Conner Humphreys", + plot: "Forrest Gump, while not intelligent, has accidentally been present at many historic moments, but his true love, Jenny Curran, eludes him.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BYThjM2MwZGMtMzg3Ny00NGRkLWE4M2EtYTBiNWMzOTY0YTI4XkEyXkFqcGdeQXVyNDYyMDk5MTU@._V1_SX300.jpg", + }, + { + id: 52, + title: "The Hobbit: The Desolation of Smaug", + year: "2013", + runtime: "161", + genres: ["Adventure", "Fantasy"], + director: "Peter Jackson", + actors: "Ian McKellen, Martin Freeman, Richard Armitage, Ken Stott", + plot: "The dwarves, along with Bilbo Baggins and Gandalf the Grey, continue their quest to reclaim Erebor, their homeland, from Smaug. Bilbo Baggins is in possession of a mysterious and magical ring.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMzU0NDY0NDEzNV5BMl5BanBnXkFtZTgwOTIxNDU1MDE@._V1_SX300.jpg", + }, + { + id: 53, + title: "Vicky Cristina Barcelona", + year: "2008", + runtime: "96", + genres: ["Drama", "Romance"], + director: "Woody Allen", + actors: + "Rebecca Hall, Scarlett Johansson, Christopher Evan Welch, Chris Messina", + plot: "Two girlfriends on a summer holiday in Spain become enamored with the same painter, unaware that his ex-wife, with whom he has a tempestuous relationship, is about to re-enter the picture.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTU2NDQ4MTg2MV5BMl5BanBnXkFtZTcwNDUzNjU3MQ@@._V1_SX300.jpg", + }, + { + id: 54, + title: "Slumdog Millionaire", + year: "2008", + runtime: "120", + genres: ["Drama", "Romance"], + director: "Danny Boyle, Loveleen Tandan", + actors: "Dev Patel, Saurabh Shukla, Anil Kapoor, Rajendranath Zutshi", + plot: 'A Mumbai teen reflects on his upbringing in the slums when he is accused of cheating on the Indian Version of "Who Wants to be a Millionaire?"', + posterUrl: + "http://ia.media-imdb.com/images/M/MV5BMTU2NTA5NzI0N15BMl5BanBnXkFtZTcwMjUxMjYxMg@@._V1_SX300.jpg", + }, + { + id: 55, + title: "Lost in Translation", + year: "2003", + runtime: "101", + genres: ["Drama"], + director: "Sofia Coppola", + actors: + "Scarlett Johansson, Bill Murray, Akiko Takeshita, Kazuyoshi Minamimagoe", + plot: "A faded movie star and a neglected young woman form an unlikely bond after crossing paths in Tokyo.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTI2NDI5ODk4N15BMl5BanBnXkFtZTYwMTI3NTE3._V1_SX300.jpg", + }, + { + id: 56, + title: "Match Point", + year: "2005", + runtime: "119", + genres: ["Drama", "Romance", "Thriller"], + director: "Woody Allen", + actors: + "Jonathan Rhys Meyers, Alexander Armstrong, Paul Kaye, Matthew Goode", + plot: "At a turning point in his life, a former tennis pro falls for an actress who happens to be dating his friend and soon-to-be brother-in-law.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTMzNzY4MzE5NF5BMl5BanBnXkFtZTcwMzQ1MDMzMQ@@._V1_SX300.jpg", + }, + { + id: 57, + title: "Psycho", + year: "1960", + runtime: "109", + genres: ["Horror", "Mystery", "Thriller"], + director: "Alfred Hitchcock", + actors: "Anthony Perkins, Vera Miles, John Gavin, Janet Leigh", + plot: "A Phoenix secretary embezzles $40,000 from her employer's client, goes on the run, and checks into a remote motel run by a young man under the domination of his mother.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMDI3OWRmOTEtOWJhYi00N2JkLTgwNGItMjdkN2U0NjFiZTYwXkEyXkFqcGdeQXVyMTQxNzMzNDI@._V1_SX300.jpg", + }, + { + id: 58, + title: "North by Northwest", + year: "1959", + runtime: "136", + genres: ["Action", "Adventure", "Crime"], + director: "Alfred Hitchcock", + actors: "Cary Grant, Eva Marie Saint, James Mason, Jessie Royce Landis", + plot: "A hapless New York advertising executive is mistaken for a government agent by a group of foreign spies, and is pursued across the country while he looks for a way to survive.", + posterUrl: + "http://ia.media-imdb.com/images/M/MV5BMjQwMTQ0MzgwNl5BMl5BanBnXkFtZTgwNjc4ODE4MzE@._V1_SX300.jpg", + }, + { + id: 59, + title: "Madagascar: Escape 2 Africa", + year: "2008", + runtime: "89", + genres: ["Animation", "Action", "Adventure"], + director: "Eric Darnell, Tom McGrath", + actors: "Ben Stiller, Chris Rock, David Schwimmer, Jada Pinkett Smith", + plot: "The animals try to fly back to New York City, but crash-land on an African wildlife refuge, where Alex is reunited with his parents.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMjExMDA4NDcwMl5BMl5BanBnXkFtZTcwODAxNTQ3MQ@@._V1_SX300.jpg", + }, + { + id: 60, + title: "Despicable Me 2", + year: "2013", + runtime: "98", + genres: ["Animation", "Adventure", "Comedy"], + director: "Pierre Coffin, Chris Renaud", + actors: "Steve Carell, Kristen Wiig, Benjamin Bratt, Miranda Cosgrove", + plot: "When Gru, the world's most super-bad turned super-dad has been recruited by a team of officials to stop lethal muscle and a host of Gru's own, He has to fight back with new gadgetry, cars, and more minion madness.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMjExNjAyNTcyMF5BMl5BanBnXkFtZTgwODQzMjQ3MDE@._V1_SX300.jpg", + }, + { + id: 61, + title: "Downfall", + year: "2004", + runtime: "156", + genres: ["Biography", "Drama", "History"], + director: "Oliver Hirschbiegel", + actors: + "Bruno Ganz, Alexandra Maria Lara, Corinna Harfouch, Ulrich Matthes", + plot: "Traudl Junge, the final secretary for Adolf Hitler, tells of the Nazi dictator's final days in his Berlin bunker at the end of WWII.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTM1OTI1MjE2Nl5BMl5BanBnXkFtZTcwMTEwMzc4NA@@._V1_SX300.jpg", + }, + { + id: 62, + title: "Madagascar", + year: "2005", + runtime: "86", + genres: ["Animation", "Adventure", "Comedy"], + director: "Eric Darnell, Tom McGrath", + actors: "Ben Stiller, Chris Rock, David Schwimmer, Jada Pinkett Smith", + plot: "Spoiled by their upbringing with no idea what wild life is really like, four animals from New York Central Zoo escape, unwittingly assisted by four absconding penguins, and find themselves in Madagascar, among a bunch of merry lemurs", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTY4NDUwMzQxMF5BMl5BanBnXkFtZTcwMDgwNjgyMQ@@._V1_SX300.jpg", + }, + { + id: 63, + title: "Madagascar 3: Europe's Most Wanted", + year: "2012", + runtime: "93", + genres: ["Animation", "Adventure", "Comedy"], + director: "Eric Darnell, Tom McGrath, Conrad Vernon", + actors: "Ben Stiller, Chris Rock, David Schwimmer, Jada Pinkett Smith", + plot: "Alex, Marty, Gloria and Melman are still fighting to get home to their beloved Big Apple. Their journey takes them through Europe where they find the perfect cover: a traveling circus, which they reinvent - Madagascar style.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTM2MTIzNzk2MF5BMl5BanBnXkFtZTcwMDcwMzQxNw@@._V1_SX300.jpg", + }, + { + id: 64, + title: "God Bless America", + year: "2011", + runtime: "105", + genres: ["Comedy", "Crime"], + director: "Bobcat Goldthwait", + actors: + "Joel Murray, Tara Lynne Barr, Melinda Page Hamilton, Mackenzie Brooke Smith", + plot: "On a mission to rid society of its most repellent citizens, terminally ill Frank makes an unlikely accomplice in 16-year-old Roxy.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTQwMTc1MzA4NF5BMl5BanBnXkFtZTcwNzQwMTgzNw@@._V1_SX300.jpg", + }, + { + id: 65, + title: "The Social Network", + year: "2010", + runtime: "120", + genres: ["Biography", "Drama"], + director: "David Fincher", + actors: "Jesse Eisenberg, Rooney Mara, Bryan Barter, Dustin Fitzsimons", + plot: "Harvard student Mark Zuckerberg creates the social networking site that would become known as Facebook, but is later sued by two brothers who claimed he stole their idea, and the co-founder who was later squeezed out of the business.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTM2ODk0NDAwMF5BMl5BanBnXkFtZTcwNTM1MDc2Mw@@._V1_SX300.jpg", + }, + { + id: 66, + title: "The Pianist", + year: "2002", + runtime: "150", + genres: ["Biography", "Drama", "War"], + director: "Roman Polanski", + actors: "Adrien Brody, Emilia Fox, Michal Zebrowski, Ed Stoppard", + plot: "A Polish Jewish musician struggles to survive the destruction of the Warsaw ghetto of World War II.", + posterUrl: + "http://ia.media-imdb.com/images/M/MV5BMTc4OTkyOTA3OF5BMl5BanBnXkFtZTYwMDIxNjk5._V1_SX300.jpg", + }, + { + id: 67, + title: "Alive", + year: "1993", + runtime: "120", + genres: ["Adventure", "Biography", "Drama"], + director: "Frank Marshall", + actors: "Ethan Hawke, Vincent Spano, Josh Hamilton, Bruce Ramsay", + plot: "Uruguayan rugby team stranded in the snow swept Andes are forced to use desperate measures to survive after a plane crash.", + posterUrl: "", + }, + { + id: 68, + title: "Casablanca", + year: "1942", + runtime: "102", + genres: ["Drama", "Romance", "War"], + director: "Michael Curtiz", + actors: "Humphrey Bogart, Ingrid Bergman, Paul Henreid, Claude Rains", + plot: "In Casablanca, Morocco in December 1941, a cynical American expatriate meets a former lover, with unforeseen complications.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMjQwNDYyNTk2N15BMl5BanBnXkFtZTgwMjQ0OTMyMjE@._V1_SX300.jpg", + }, + { + id: 69, + title: "American Gangster", + year: "2007", + runtime: "157", + genres: ["Biography", "Crime", "Drama"], + director: "Ridley Scott", + actors: "Denzel Washington, Russell Crowe, Chiwetel Ejiofor, Josh Brolin", + plot: "In 1970s America, a detective works to bring down the drug empire of Frank Lucas, a heroin kingpin from Manhattan, who is smuggling the drug into the country from the Far East.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTkyNzY5MDA5MV5BMl5BanBnXkFtZTcwMjg4MzI3MQ@@._V1_SX300.jpg", + }, + { + id: 70, + title: "Catch Me If You Can", + year: "2002", + runtime: "141", + genres: ["Biography", "Crime", "Drama"], + director: "Steven Spielberg", + actors: "Leonardo DiCaprio, Tom Hanks, Christopher Walken, Martin Sheen", + plot: "The true story of Frank Abagnale Jr. who, before his 19th birthday, successfully conned millions of dollars' worth of checks as a Pan Am pilot, doctor, and legal prosecutor.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTY5MzYzNjc5NV5BMl5BanBnXkFtZTYwNTUyNTc2._V1_SX300.jpg", + }, + { + id: 71, + title: "American History X", + year: "1998", + runtime: "119", + genres: ["Crime", "Drama"], + director: "Tony Kaye", + actors: "Edward Norton, Edward Furlong, Beverly D'Angelo, Jennifer Lien", + plot: "A former neo-nazi skinhead tries to prevent his younger brother from going down the same wrong path that he did.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BZjA0MTM4MTQtNzY5MC00NzY3LWI1ZTgtYzcxMjkyMzU4MDZiXkEyXkFqcGdeQXVyNDYyMDk5MTU@._V1_SX300.jpg", + }, + { + id: 72, + title: "Casino", + year: "1995", + runtime: "178", + genres: ["Biography", "Crime", "Drama"], + director: "Martin Scorsese", + actors: "Robert De Niro, Sharon Stone, Joe Pesci, James Woods", + plot: "Greed, deception, money, power, and murder occur between two best friends, a mafia underboss and a casino owner, for a trophy wife over a gambling empire.", + posterUrl: + "http://ia.media-imdb.com/images/M/MV5BMTcxOWYzNDYtYmM4YS00N2NkLTk0NTAtNjg1ODgwZjAxYzI3XkEyXkFqcGdeQXVyNTA4NzY1MzY@._V1_SX300.jpg", + }, + { + id: 73, + title: "Pirates of the Caribbean: At World's End", + year: "2007", + runtime: "169", + genres: ["Action", "Adventure", "Fantasy"], + director: "Gore Verbinski", + actors: "Johnny Depp, Geoffrey Rush, Orlando Bloom, Keira Knightley", + plot: "Captain Barbossa, Will Turner and Elizabeth Swann must sail off the edge of the map, navigate treachery and betrayal, find Jack Sparrow, and make their final alliances for one last decisive battle.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMjIyNjkxNzEyMl5BMl5BanBnXkFtZTYwMjc3MDE3._V1_SX300.jpg", + }, + { + id: 74, + title: "Pirates of the Caribbean: On Stranger Tides", + year: "2011", + runtime: "136", + genres: ["Action", "Adventure", "Fantasy"], + director: "Rob Marshall", + actors: "Johnny Depp, Penélope Cruz, Geoffrey Rush, Ian McShane", + plot: "Jack Sparrow and Barbossa embark on a quest to find the elusive fountain of youth, only to discover that Blackbeard and his daughter are after it too.", + posterUrl: + "http://ia.media-imdb.com/images/M/MV5BMjE5MjkwODI3Nl5BMl5BanBnXkFtZTcwNjcwMDk4NA@@._V1_SX300.jpg", + }, + { + id: 75, + title: "Crash", + year: "2004", + runtime: "112", + genres: ["Crime", "Drama", "Thriller"], + director: "Paul Haggis", + actors: "Karina Arroyave, Dato Bakhtadze, Sandra Bullock, Don Cheadle", + plot: "Los Angeles citizens with vastly separate lives collide in interweaving stories of race, loss and redemption.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BOTk1OTA1MjIyNV5BMl5BanBnXkFtZTcwODQxMTkyMQ@@._V1_SX300.jpg", + }, + { + id: 76, + title: "Pirates of the Caribbean: The Curse of the Black Pearl", + year: "2003", + runtime: "143", + genres: ["Action", "Adventure", "Fantasy"], + director: "Gore Verbinski", + actors: "Johnny Depp, Geoffrey Rush, Orlando Bloom, Keira Knightley", + plot: "Blacksmith Will Turner teams up with eccentric pirate \"Captain\" Jack Sparrow to save his love, the governor's daughter, from Jack's former pirate allies, who are now undead.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMjAyNDM4MTc2N15BMl5BanBnXkFtZTYwNDk0Mjc3._V1_SX300.jpg", + }, + { + id: 77, + title: "The Lord of the Rings: The Return of the King", + year: "2003", + runtime: "201", + genres: ["Action", "Adventure", "Drama"], + director: "Peter Jackson", + actors: "Noel Appleby, Ali Astin, Sean Astin, David Aston", + plot: "Gandalf and Aragorn lead the World of Men against Sauron's army to draw his gaze from Frodo and Sam as they approach Mount Doom with the One Ring.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMjE4MjA1NTAyMV5BMl5BanBnXkFtZTcwNzM1NDQyMQ@@._V1_SX300.jpg", + }, + { + id: 78, + title: "Oldboy", + year: "2003", + runtime: "120", + genres: ["Drama", "Mystery", "Thriller"], + director: "Chan-wook Park", + actors: "Min-sik Choi, Ji-tae Yu, Hye-jeong Kang, Dae-han Ji", + plot: "After being kidnapped and imprisoned for 15 years, Oh Dae-Su is released, only to find that he must find his captor in 5 days.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTI3NTQyMzU5M15BMl5BanBnXkFtZTcwMTM2MjgyMQ@@._V1_SX300.jpg", + }, + { + id: 79, + title: "Chocolat", + year: "2000", + runtime: "121", + genres: ["Drama", "Romance"], + director: "Lasse Hallström", + actors: + "Alfred Molina, Carrie-Anne Moss, Aurelien Parent Koenig, Antonio Gil", + plot: "A woman and her daughter open a chocolate shop in a small French village that shakes up the rigid morality of the community.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMjA4MDI3NTQwMV5BMl5BanBnXkFtZTcwNjIzNDcyMQ@@._V1_SX300.jpg", + }, + { + id: 80, + title: "Casino Royale", + year: "2006", + runtime: "144", + genres: ["Action", "Adventure", "Thriller"], + director: "Martin Campbell", + actors: "Daniel Craig, Eva Green, Mads Mikkelsen, Judi Dench", + plot: "Armed with a licence to kill, Secret Agent James Bond sets out on his first mission as 007 and must defeat a weapons dealer in a high stakes game of poker at Casino Royale, but things are not what they seem.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTM5MjI4NDExNF5BMl5BanBnXkFtZTcwMDM1MjMzMQ@@._V1_SX300.jpg", + }, + { + id: 81, + title: "WALL·E", + year: "2008", + runtime: "98", + genres: ["Animation", "Adventure", "Family"], + director: "Andrew Stanton", + actors: "Ben Burtt, Elissa Knight, Jeff Garlin, Fred Willard", + plot: "In the distant future, a small waste-collecting robot inadvertently embarks on a space journey that will ultimately decide the fate of mankind.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTczOTA3MzY2N15BMl5BanBnXkFtZTcwOTYwNjE2MQ@@._V1_SX300.jpg", + }, + { + id: 82, + title: "The Wolf of Wall Street", + year: "2013", + runtime: "180", + genres: ["Biography", "Comedy", "Crime"], + director: "Martin Scorsese", + actors: "Leonardo DiCaprio, Jonah Hill, Margot Robbie, Matthew McConaughey", + plot: "Based on the true story of Jordan Belfort, from his rise to a wealthy stock-broker living the high life to his fall involving crime, corruption and the federal government.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMjIxMjgxNTk0MF5BMl5BanBnXkFtZTgwNjIyOTg2MDE@._V1_SX300.jpg", + }, + { + id: 83, + title: "Hellboy II: The Golden Army", + year: "2008", + runtime: "120", + genres: ["Action", "Adventure", "Fantasy"], + director: "Guillermo del Toro", + actors: "Ron Perlman, Selma Blair, Doug Jones, John Alexander", + plot: "The mythical world starts a rebellion against humanity in order to rule the Earth, so Hellboy and his team must save the world from the rebellious creatures.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMjA5NzgyMjc2Nl5BMl5BanBnXkFtZTcwOTU3MDI3MQ@@._V1_SX300.jpg", + }, + { + id: 84, + title: "Sunset Boulevard", + year: "1950", + runtime: "110", + genres: ["Drama", "Film-Noir", "Romance"], + director: "Billy Wilder", + actors: "William Holden, Gloria Swanson, Erich von Stroheim, Nancy Olson", + plot: "A hack screenwriter writes a screenplay for a former silent-film star who has faded into Hollywood obscurity.", + posterUrl: + "http://ia.media-imdb.com/images/M/MV5BMTc3NDYzODAwNV5BMl5BanBnXkFtZTgwODg1MTczMTE@._V1_SX300.jpg", + }, + { + id: 85, + title: "I-See-You.Com", + year: "2006", + runtime: "92", + genres: ["Comedy"], + director: "Eric Steven Stahl", + actors: "Beau Bridges, Rosanna Arquette, Mathew Botuchis, Shiri Appleby", + plot: "A 17-year-old boy buys mini-cameras and displays the footage online at I-see-you.com. The cash rolls in as the site becomes a major hit. Everyone seems to have fun until it all comes crashing down....", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTYwMDUzNzA5Nl5BMl5BanBnXkFtZTcwMjQ2Njk3MQ@@._V1_SX300.jpg", + }, + { + id: 86, + title: "The Grand Budapest Hotel", + year: "2014", + runtime: "99", + genres: ["Adventure", "Comedy", "Crime"], + director: "Wes Anderson", + actors: "Ralph Fiennes, F. Murray Abraham, Mathieu Amalric, Adrien Brody", + plot: "The adventures of Gustave H, a legendary concierge at a famous hotel from the fictional Republic of Zubrowka between the first and second World Wars, and Zero Moustafa, the lobby boy who becomes his most trusted friend.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMzM5NjUxOTEyMl5BMl5BanBnXkFtZTgwNjEyMDM0MDE@._V1_SX300.jpg", + }, + { + id: 87, + title: "The Hitchhiker's Guide to the Galaxy", + year: "2005", + runtime: "109", + genres: ["Adventure", "Comedy", "Sci-Fi"], + director: "Garth Jennings", + actors: "Bill Bailey, Anna Chancellor, Warwick Davis, Yasiin Bey", + plot: 'Mere seconds before the Earth is to be demolished by an alien construction crew, journeyman Arthur Dent is swept off the planet by his friend Ford Prefect, a researcher penning a new edition of "The Hitchhiker\'s Guide to the Galaxy."', + posterUrl: + "http://ia.media-imdb.com/images/M/MV5BMjEwOTk4NjU2MF5BMl5BanBnXkFtZTYwMDA3NzI3._V1_SX300.jpg", + }, + { + id: 88, + title: "Once Upon a Time in America", + year: "1984", + runtime: "229", + genres: ["Crime", "Drama"], + director: "Sergio Leone", + actors: "Robert De Niro, James Woods, Elizabeth McGovern, Joe Pesci", + plot: "A former Prohibition-era Jewish gangster returns to the Lower East Side of Manhattan over thirty years later, where he once again must confront the ghosts and regrets of his old life.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMGFkNWI4MTMtNGQ0OC00MWVmLTk3MTktOGYxN2Y2YWVkZWE2XkEyXkFqcGdeQXVyNjU0OTQ0OTY@._V1_SX300.jpg", + }, + { + id: 89, + title: "Oblivion", + year: "2013", + runtime: "124", + genres: ["Action", "Adventure", "Mystery"], + director: "Joseph Kosinski", + actors: "Tom Cruise, Morgan Freeman, Olga Kurylenko, Andrea Riseborough", + plot: "A veteran assigned to extract Earth's remaining resources begins to question what he knows about his mission and himself.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTQwMDY0MTA4MF5BMl5BanBnXkFtZTcwNzI3MDgxOQ@@._V1_SX300.jpg", + }, + { + id: 90, + title: "V for Vendetta", + year: "2005", + runtime: "132", + genres: ["Action", "Drama", "Thriller"], + director: "James McTeigue", + actors: "Natalie Portman, Hugo Weaving, Stephen Rea, Stephen Fry", + plot: 'In a future British tyranny, a shadowy freedom fighter, known only by the alias of "V", plots to overthrow it with the help of a young woman.', + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BOTI5ODc3NzExNV5BMl5BanBnXkFtZTcwNzYxNzQzMw@@._V1_SX300.jpg", + }, + { + id: 91, + title: "Gattaca", + year: "1997", + runtime: "106", + genres: ["Drama", "Sci-Fi", "Thriller"], + director: "Andrew Niccol", + actors: "Ethan Hawke, Uma Thurman, Gore Vidal, Xander Berkeley", + plot: "A genetically inferior man assumes the identity of a superior one in order to pursue his lifelong dream of space travel.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BNDQxOTc0MzMtZmRlOS00OWQ5LWI2ZDctOTAwNmMwOTYxYzlhXkEyXkFqcGdeQXVyMTQxNzMzNDI@._V1_SX300.jpg", + }, + { + id: 92, + title: "Silver Linings Playbook", + year: "2012", + runtime: "122", + genres: ["Comedy", "Drama", "Romance"], + director: "David O. Russell", + actors: "Bradley Cooper, Jennifer Lawrence, Robert De Niro, Jacki Weaver", + plot: "After a stint in a mental institution, former teacher Pat Solitano moves back in with his parents and tries to reconcile with his ex-wife. Things get more challenging when Pat meets Tiffany, a mysterious girl with problems of her own.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTM2MTI5NzA3MF5BMl5BanBnXkFtZTcwODExNTc0OA@@._V1_SX300.jpg", + }, + { + id: 93, + title: "Alice in Wonderland", + year: "2010", + runtime: "108", + genres: ["Adventure", "Family", "Fantasy"], + director: "Tim Burton", + actors: "Johnny Depp, Mia Wasikowska, Helena Bonham Carter, Anne Hathaway", + plot: "Nineteen-year-old Alice returns to the magical world from her childhood adventure, where she reunites with her old friends and learns of her true destiny: to end the Red Queen's reign of terror.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTMwNjAxMTc0Nl5BMl5BanBnXkFtZTcwODc3ODk5Mg@@._V1_SX300.jpg", + }, + { + id: 94, + title: "Gandhi", + year: "1982", + runtime: "191", + genres: ["Biography", "Drama"], + director: "Richard Attenborough", + actors: "Ben Kingsley, Candice Bergen, Edward Fox, John Gielgud", + plot: "Gandhi's character is fully explained as a man of nonviolence. Through his patience, he is able to drive the British out of the subcontinent. And the stubborn nature of Jinnah and his commitment towards Pakistan is portrayed.", + posterUrl: + "http://ia.media-imdb.com/images/M/MV5BMzJiZDRmOWUtYjE2MS00Mjc1LTg1ZDYtNTQxYWJkZTg1OTM4XkEyXkFqcGdeQXVyNjUwNzk3NDc@._V1_SX300.jpg", + }, + { + id: 95, + title: "Pacific Rim", + year: "2013", + runtime: "131", + genres: ["Action", "Adventure", "Sci-Fi"], + director: "Guillermo del Toro", + actors: "Charlie Hunnam, Diego Klattenhoff, Idris Elba, Rinko Kikuchi", + plot: "As a war between humankind and monstrous sea creatures wages on, a former pilot and a trainee are paired up to drive a seemingly obsolete special weapon in a desperate effort to save the world from the apocalypse.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTY3MTI5NjQ4Nl5BMl5BanBnXkFtZTcwOTU1OTU0OQ@@._V1_SX300.jpg", + }, + { + id: 96, + title: "Kiss Kiss Bang Bang", + year: "2005", + runtime: "103", + genres: ["Comedy", "Crime", "Mystery"], + director: "Shane Black", + actors: "Robert Downey Jr., Val Kilmer, Michelle Monaghan, Corbin Bernsen", + plot: "A murder mystery brings together a private eye, a struggling actress, and a thief masquerading as an actor.", + posterUrl: + "http://ia.media-imdb.com/images/M/MV5BMTY5NDExMDA3M15BMl5BanBnXkFtZTYwNTc2MzA3._V1_SX300.jpg", + }, + { + id: 97, + title: "The Quiet American", + year: "2002", + runtime: "101", + genres: ["Drama", "Mystery", "Romance"], + director: "Phillip Noyce", + actors: "Michael Caine, Brendan Fraser, Do Thi Hai Yen, Rade Serbedzija", + plot: "An older British reporter vies with a young U.S. doctor for the affections of a beautiful Vietnamese woman.", + posterUrl: + "http://ia.media-imdb.com/images/M/MV5BMjE2NTUxNTE3Nl5BMl5BanBnXkFtZTYwNTczMTg5._V1_SX300.jpg", + }, + { + id: 98, + title: "Cloud Atlas", + year: "2012", + runtime: "172", + genres: ["Drama", "Sci-Fi"], + director: "Tom Tykwer, Lana Wachowski, Lilly Wachowski", + actors: "Tom Hanks, Halle Berry, Jim Broadbent, Hugo Weaving", + plot: "An exploration of how the actions of individual lives impact one another in the past, present and future, as one soul is shaped from a killer into a hero, and an act of kindness ripples across centuries to inspire a revolution.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTczMTgxMjc4NF5BMl5BanBnXkFtZTcwNjM5MTA2OA@@._V1_SX300.jpg", + }, + { + id: 99, + title: "The Impossible", + year: "2012", + runtime: "114", + genres: ["Drama", "Thriller"], + director: "J.A. Bayona", + actors: "Naomi Watts, Ewan McGregor, Tom Holland, Samuel Joslin", + plot: "The story of a tourist family in Thailand caught in the destruction and chaotic aftermath of the 2004 Indian Ocean tsunami.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMjA5NTA3NzQ5Nl5BMl5BanBnXkFtZTcwOTYxNjY0OA@@._V1_SX300.jpg", + }, + { + id: 100, + title: "All Quiet on the Western Front", + year: "1930", + runtime: "136", + genres: ["Drama", "War"], + director: "Lewis Milestone", + actors: "Louis Wolheim, Lew Ayres, John Wray, Arnold Lucy", + plot: "A young soldier faces profound disillusionment in the soul-destroying horror of World War I.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BNTM5OTg2NDY1NF5BMl5BanBnXkFtZTcwNTQ4MTMwNw@@._V1_SX300.jpg", + }, + { + id: 101, + title: "The English Patient", + year: "1996", + runtime: "162", + genres: ["Drama", "Romance", "War"], + director: "Anthony Minghella", + actors: + "Ralph Fiennes, Juliette Binoche, Willem Dafoe, Kristin Scott Thomas", + plot: "At the close of WWII, a young nurse tends to a badly-burned plane crash victim. His past is shown in flashbacks, revealing an involvement in a fateful love affair.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BNDg2OTcxNDE0OF5BMl5BanBnXkFtZTgwOTg2MDM0MDE@._V1_SX300.jpg", + }, + { + id: 102, + title: "Dallas Buyers Club", + year: "2013", + runtime: "117", + genres: ["Biography", "Drama"], + director: "Jean-Marc Vallée", + actors: "Matthew McConaughey, Jennifer Garner, Jared Leto, Denis O'Hare", + plot: "In 1985 Dallas, electrician and hustler Ron Woodroof works around the system to help AIDS patients get the medication they need after he is diagnosed with the disease.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTYwMTA4MzgyNF5BMl5BanBnXkFtZTgwMjEyMjE0MDE@._V1_SX300.jpg", + }, + { + id: 103, + title: "Frida", + year: "2002", + runtime: "123", + genres: ["Biography", "Drama", "Romance"], + director: "Julie Taymor", + actors: "Salma Hayek, Mía Maestro, Alfred Molina, Antonio Banderas", + plot: "A biography of artist Frida Kahlo, who channeled the pain of a crippling injury and her tempestuous marriage into her work.", + posterUrl: + "http://ia.media-imdb.com/images/M/MV5BMTMyODUyMDY1OV5BMl5BanBnXkFtZTYwMDA2OTU3._V1_SX300.jpg", + }, + { + id: 104, + title: "Before Sunrise", + year: "1995", + runtime: "105", + genres: ["Drama", "Romance"], + director: "Richard Linklater", + actors: "Ethan Hawke, Julie Delpy, Andrea Eckert, Hanno Pöschl", + plot: "A young man and woman meet on a train in Europe, and wind up spending one evening together in Vienna. Unfortunately, both know that this will probably be their only night together.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTQyMTM3MTQxMl5BMl5BanBnXkFtZTcwMDAzNjQ4Mg@@._V1_SX300.jpg", + }, + { + id: 105, + title: "The Rum Diary", + year: "2011", + runtime: "120", + genres: ["Comedy", "Drama"], + director: "Bruce Robinson", + actors: "Johnny Depp, Aaron Eckhart, Michael Rispoli, Amber Heard", + plot: "American journalist Paul Kemp takes on a freelance job in Puerto Rico for a local newspaper during the 1960s and struggles to find a balance between island culture and the expatriates who live there.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTM5ODA4MjYxM15BMl5BanBnXkFtZTcwMTM3NTE5Ng@@._V1_SX300.jpg", + }, + { + id: 106, + title: "The Last Samurai", + year: "2003", + runtime: "154", + genres: ["Action", "Drama", "History"], + director: "Edward Zwick", + actors: "Ken Watanabe, Tom Cruise, William Atherton, Chad Lindberg", + plot: "An American military advisor embraces the Samurai culture he was hired to destroy after he is captured in battle.", + posterUrl: + "http://ia.media-imdb.com/images/M/MV5BMzkyNzQ1Mzc0NV5BMl5BanBnXkFtZTcwODg3MzUzMw@@._V1_SX300.jpg", + }, + { + id: 107, + title: "Chinatown", + year: "1974", + runtime: "130", + genres: ["Drama", "Mystery", "Thriller"], + director: "Roman Polanski", + actors: "Jack Nicholson, Faye Dunaway, John Huston, Perry Lopez", + plot: "A private detective hired to expose an adulterer finds himself caught up in a web of deceit, corruption and murder.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BN2YyNDE5NzItMjAwNC00MGQxLTllNjktZGIzMWFkZjA3OWQ0XkEyXkFqcGdeQXVyNDk3NzU2MTQ@._V1_SX300.jpg", + }, + { + id: 108, + title: "Calvary", + year: "2014", + runtime: "102", + genres: ["Comedy", "Drama"], + director: "John Michael McDonagh", + actors: "Brendan Gleeson, Chris O'Dowd, Kelly Reilly, Aidan Gillen", + plot: "After he is threatened during a confession, a good-natured priest must battle the dark forces closing in around him.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTc3MjQ1MjE2M15BMl5BanBnXkFtZTgwNTMzNjE4MTE@._V1_SX300.jpg", + }, + { + id: 109, + title: "Before Sunset", + year: "2004", + runtime: "80", + genres: ["Drama", "Romance"], + director: "Richard Linklater", + actors: "Ethan Hawke, Julie Delpy, Vernon Dobtcheff, Louise Lemoine Torrès", + plot: "Nine years after Jesse and Celine first met, they encounter each other again on the French leg of Jesse's book tour.", + posterUrl: + "http://ia.media-imdb.com/images/M/MV5BMTQ1MjAwNTM5Ml5BMl5BanBnXkFtZTYwNDM0MTc3._V1_SX300.jpg", + }, + { + id: 110, + title: "Spirited Away", + year: "2001", + runtime: "125", + genres: ["Animation", "Adventure", "Family"], + director: "Hayao Miyazaki", + actors: "Rumi Hiiragi, Miyu Irino, Mari Natsuki, Takashi Naitô", + plot: "During her family's move to the suburbs, a sullen 10-year-old girl wanders into a world ruled by gods, witches, and spirits, and where humans are changed into beasts.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMjYxMDcyMzIzNl5BMl5BanBnXkFtZTYwNDg2MDU3._V1_SX300.jpg", + }, + { + id: 111, + title: "Indochine", + year: "1992", + runtime: "159", + genres: ["Drama", "Romance"], + director: "Régis Wargnier", + actors: "Catherine Deneuve, Vincent Perez, Linh Dan Pham, Jean Yanne", + plot: "This story is set in 1930, at the time when French colonial rule in Indochina is ending. A widowed French woman who works in the rubber fields, raises a Vietnamese princess as if she was ...", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTM1MTkzNzA3NF5BMl5BanBnXkFtZTYwNTI2MzU5._V1_SX300.jpg", + }, + { + id: 112, + title: "Birdman or (The Unexpected Virtue of Ignorance)", + year: "2014", + runtime: "119", + genres: ["Comedy", "Drama", "Romance"], + director: "Alejandro G. Iñárritu", + actors: "Michael Keaton, Emma Stone, Kenny Chin, Jamahl Garrison-Lowe", + plot: "Illustrated upon the progress of his latest Broadway play, a former popular actor's struggle to cope with his current life as a wasted actor is shown.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BODAzNDMxMzAxOV5BMl5BanBnXkFtZTgwMDMxMjA4MjE@._V1_SX300.jpg", + }, + { + id: 113, + title: "Boyhood", + year: "2014", + runtime: "165", + genres: ["Drama"], + director: "Richard Linklater", + actors: + "Ellar Coltrane, Patricia Arquette, Elijah Smith, Lorelei Linklater", + plot: "The life of Mason, from early childhood to his arrival at college.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTYzNDc2MDc0N15BMl5BanBnXkFtZTgwOTcwMDQ5MTE@._V1_SX300.jpg", + }, + { + id: 114, + title: "12 Angry Men", + year: "1957", + runtime: "96", + genres: ["Crime", "Drama"], + director: "Sidney Lumet", + actors: "Martin Balsam, John Fiedler, Lee J. Cobb, E.G. Marshall", + plot: "A jury holdout attempts to prevent a miscarriage of justice by forcing his colleagues to reconsider the evidence.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BODQwOTc5MDM2N15BMl5BanBnXkFtZTcwODQxNTEzNA@@._V1_SX300.jpg", + }, + { + id: 115, + title: "The Imitation Game", + year: "2014", + runtime: "114", + genres: ["Biography", "Drama", "Thriller"], + director: "Morten Tyldum", + actors: + "Benedict Cumberbatch, Keira Knightley, Matthew Goode, Rory Kinnear", + plot: "During World War II, mathematician Alan Turing tries to crack the enigma code with help from fellow mathematicians.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BNDkwNTEyMzkzNl5BMl5BanBnXkFtZTgwNTAwNzk3MjE@._V1_SX300.jpg", + }, + { + id: 116, + title: "Interstellar", + year: "2014", + runtime: "169", + genres: ["Adventure", "Drama", "Sci-Fi"], + director: "Christopher Nolan", + actors: "Ellen Burstyn, Matthew McConaughey, Mackenzie Foy, John Lithgow", + plot: "A team of explorers travel through a wormhole in space in an attempt to ensure humanity's survival.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMjIxNTU4MzY4MF5BMl5BanBnXkFtZTgwMzM4ODI3MjE@._V1_SX300.jpg", + }, + { + id: 117, + title: "Big Nothing", + year: "2006", + runtime: "86", + genres: ["Comedy", "Crime", "Thriller"], + director: "Jean-Baptiste Andrea", + actors: "David Schwimmer, Simon Pegg, Alice Eve, Natascha McElhone", + plot: "A frustrated, unemployed teacher joining forces with a scammer and his girlfriend in a blackmailing scheme.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTY5NTc2NjYwOV5BMl5BanBnXkFtZTcwMzk5OTY0MQ@@._V1_SX300.jpg", + }, + { + id: 118, + title: "Das Boot", + year: "1981", + runtime: "149", + genres: ["Adventure", "Drama", "Thriller"], + director: "Wolfgang Petersen", + actors: + "Jürgen Prochnow, Herbert Grönemeyer, Klaus Wennemann, Hubertus Bengsch", + plot: "The claustrophobic world of a WWII German U-boat; boredom, filth, and sheer terror.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMjE5Mzk5OTQ0Nl5BMl5BanBnXkFtZTYwNzUwMTQ5._V1_SX300.jpg", + }, + { + id: 119, + title: "Shrek 2", + year: "2004", + runtime: "93", + genres: ["Animation", "Adventure", "Comedy"], + director: "Andrew Adamson, Kelly Asbury, Conrad Vernon", + actors: "Mike Myers, Eddie Murphy, Cameron Diaz, Julie Andrews", + plot: "Princess Fiona's parents invite her and Shrek to dinner to celebrate her marriage. If only they knew the newlyweds were both ogres.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTk4MTMwNjI4M15BMl5BanBnXkFtZTcwMjExMzUyMQ@@._V1_SX300.jpg", + }, + { + id: 120, + title: "Sin City", + year: "2005", + runtime: "124", + genres: ["Crime", "Thriller"], + director: "Frank Miller, Robert Rodriguez, Quentin Tarantino", + actors: "Jessica Alba, Devon Aoki, Alexis Bledel, Powers Boothe", + plot: "A film that explores the dark and miserable town, Basin City, and tells the story of three different people, all caught up in violent corruption.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BODZmYjMwNzEtNzVhNC00ZTRmLTk2M2UtNzE1MTQ2ZDAxNjc2XkEyXkFqcGdeQXVyMTQxNzMzNDI@._V1_SX300.jpg", + }, + { + id: 121, + title: "Nebraska", + year: "2013", + runtime: "115", + genres: ["Adventure", "Comedy", "Drama"], + director: "Alexander Payne", + actors: "Bruce Dern, Will Forte, June Squibb, Bob Odenkirk", + plot: "An aging, booze-addled father makes the trip from Montana to Nebraska with his estranged son in order to claim a million-dollar Mega Sweepstakes Marketing prize.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTU2Mjk2NDkyMl5BMl5BanBnXkFtZTgwNTk0NzcyMDE@._V1_SX300.jpg", + }, + { + id: 122, + title: "Shrek", + year: "2001", + runtime: "90", + genres: ["Animation", "Adventure", "Comedy"], + director: "Andrew Adamson, Vicky Jenson", + actors: "Mike Myers, Eddie Murphy, Cameron Diaz, John Lithgow", + plot: "After his swamp is filled with magical creatures, an ogre agrees to rescue a princess for a villainous lord in order to get his land back.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTk2NTE1NTE0M15BMl5BanBnXkFtZTgwNjY4NTYxMTE@._V1_SX300.jpg", + }, + { + id: 123, + title: "Mr. & Mrs. Smith", + year: "2005", + runtime: "120", + genres: ["Action", "Comedy", "Crime"], + director: "Doug Liman", + actors: "Brad Pitt, Angelina Jolie, Vince Vaughn, Adam Brody", + plot: "A bored married couple is surprised to learn that they are both assassins hired by competing agencies to kill each other.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTUxMzcxNzQzOF5BMl5BanBnXkFtZTcwMzQxNjUyMw@@._V1_SX300.jpg", + }, + { + id: 124, + title: "Original Sin", + year: "2001", + runtime: "116", + genres: ["Drama", "Mystery", "Romance"], + director: "Michael Cristofer", + actors: "Antonio Banderas, Angelina Jolie, Thomas Jane, Jack Thompson", + plot: "A woman along with her lover, plan to con a rich man by marrying him and on earning his trust running away with all his money. Everything goes as planned until she actually begins to fall in love with him.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BODg3Mjg0MDY4M15BMl5BanBnXkFtZTcwNjY5MDQ2NA@@._V1_SX300.jpg", + }, + { + id: 125, + title: "Shrek Forever After", + year: "2010", + runtime: "93", + genres: ["Animation", "Adventure", "Comedy"], + director: "Mike Mitchell", + actors: "Mike Myers, Eddie Murphy, Cameron Diaz, Antonio Banderas", + plot: "Rumpelstiltskin tricks a mid-life crisis burdened Shrek into allowing himself to be erased from existence and cast in a dark alternate timeline where Rumpel rules supreme.", + posterUrl: + "http://ia.media-imdb.com/images/M/MV5BMTY0OTU1NzkxMl5BMl5BanBnXkFtZTcwMzI2NDUzMw@@._V1_SX300.jpg", + }, + { + id: 126, + title: "Before Midnight", + year: "2013", + runtime: "109", + genres: ["Drama", "Romance"], + director: "Richard Linklater", + actors: + "Ethan Hawke, Julie Delpy, Seamus Davey-Fitzpatrick, Jennifer Prior", + plot: "We meet Jesse and Celine nine years on in Greece. Almost two decades have passed since their first meeting on that train bound for Vienna.", + posterUrl: + "http://ia.media-imdb.com/images/M/MV5BMjA5NzgxODE2NF5BMl5BanBnXkFtZTcwNTI1NTI0OQ@@._V1_SX300.jpg", + }, + { + id: 127, + title: "Despicable Me", + year: "2010", + runtime: "95", + genres: ["Animation", "Adventure", "Comedy"], + director: "Pierre Coffin, Chris Renaud", + actors: "Steve Carell, Jason Segel, Russell Brand, Julie Andrews", + plot: "When a criminal mastermind uses a trio of orphan girls as pawns for a grand scheme, he finds their love is profoundly changing him for the better.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTY3NjY0MTQ0Nl5BMl5BanBnXkFtZTcwMzQ2MTc0Mw@@._V1_SX300.jpg", + }, + { + id: 128, + title: "Troy", + year: "2004", + runtime: "163", + genres: ["Adventure"], + director: "Wolfgang Petersen", + actors: "Julian Glover, Brian Cox, Nathan Jones, Adoni Maropis", + plot: "An adaptation of Homer's great epic, the film follows the assault on Troy by the united Greek forces and chronicles the fates of the men involved.", + posterUrl: + "http://ia.media-imdb.com/images/M/MV5BMTk5MzU1MDMwMF5BMl5BanBnXkFtZTcwNjczODMzMw@@._V1_SX300.jpg", + }, + { + id: 129, + title: "The Hobbit: An Unexpected Journey", + year: "2012", + runtime: "169", + genres: ["Adventure", "Fantasy"], + director: "Peter Jackson", + actors: "Ian McKellen, Martin Freeman, Richard Armitage, Ken Stott", + plot: "A reluctant hobbit, Bilbo Baggins, sets out to the Lonely Mountain with a spirited group of dwarves to reclaim their mountain home - and the gold within it - from the dragon Smaug.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTcwNTE4MTUxMl5BMl5BanBnXkFtZTcwMDIyODM4OA@@._V1_SX300.jpg", + }, + { + id: 130, + title: "The Great Gatsby", + year: "2013", + runtime: "143", + genres: ["Drama", "Romance"], + director: "Baz Luhrmann", + actors: "Lisa Adam, Frank Aldridge, Amitabh Bachchan, Steve Bisley", + plot: "A writer and wall street trader, Nick, finds himself drawn to the past and lifestyle of his millionaire neighbor, Jay Gatsby.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTkxNTk1ODcxNl5BMl5BanBnXkFtZTcwMDI1OTMzOQ@@._V1_SX300.jpg", + }, + { + id: 131, + title: "Ice Age", + year: "2002", + runtime: "81", + genres: ["Animation", "Adventure", "Comedy"], + director: "Chris Wedge, Carlos Saldanha", + actors: "Ray Romano, John Leguizamo, Denis Leary, Goran Visnjic", + plot: "Set during the Ice Age, a sabertooth tiger, a sloth, and a wooly mammoth find a lost human infant, and they try to return him to his tribe.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMjEyNzI1ODA0MF5BMl5BanBnXkFtZTYwODIxODY3._V1_SX300.jpg", + }, + { + id: 132, + title: "The Lord of the Rings: The Fellowship of the Ring", + year: "2001", + runtime: "178", + genres: ["Action", "Adventure", "Drama"], + director: "Peter Jackson", + actors: "Alan Howard, Noel Appleby, Sean Astin, Sala Baker", + plot: "A meek Hobbit from the Shire and eight companions set out on a journey to destroy the powerful One Ring and save Middle Earth from the Dark Lord Sauron.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BNTEyMjAwMDU1OV5BMl5BanBnXkFtZTcwNDQyNTkxMw@@._V1_SX300.jpg", + }, + { + id: 133, + title: "The Lord of the Rings: The Two Towers", + year: "2002", + runtime: "179", + genres: ["Action", "Adventure", "Drama"], + director: "Peter Jackson", + actors: "Bruce Allpress, Sean Astin, John Bach, Sala Baker", + plot: "While Frodo and Sam edge closer to Mordor with the help of the shifty Gollum, the divided fellowship makes a stand against Sauron's new ally, Saruman, and his hordes of Isengard.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTAyNDU0NjY4NTheQTJeQWpwZ15BbWU2MDk4MTY2Nw@@._V1_SX300.jpg", + }, + { + id: 134, + title: "Ex Machina", + year: "2015", + runtime: "108", + genres: ["Drama", "Mystery", "Sci-Fi"], + director: "Alex Garland", + actors: "Domhnall Gleeson, Corey Johnson, Oscar Isaac, Alicia Vikander", + plot: "A young programmer is selected to participate in a ground-breaking experiment in synthetic intelligence by evaluating the human qualities of a breath-taking humanoid A.I.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTUxNzc0OTIxMV5BMl5BanBnXkFtZTgwNDI3NzU2NDE@._V1_SX300.jpg", + }, + { + id: 135, + title: "The Theory of Everything", + year: "2014", + runtime: "123", + genres: ["Biography", "Drama", "Romance"], + director: "James Marsh", + actors: "Eddie Redmayne, Felicity Jones, Tom Prior, Sophie Perry", + plot: "A look at the relationship between the famous physicist Stephen Hawking and his wife.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTAwMTU4MDA3NDNeQTJeQWpwZ15BbWU4MDk4NTMxNTIx._V1_SX300.jpg", + }, + { + id: 136, + title: "Shogun", + year: "1980", + runtime: "60", + genres: ["Adventure", "Drama", "History"], + director: "N/A", + actors: "Richard Chamberlain, Toshirô Mifune, Yôko Shimada, Furankî Sakai", + plot: "A English navigator becomes both a player and pawn in the complex political games in feudal Japan.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTY1ODI4NzYxMl5BMl5BanBnXkFtZTcwNDA4MzUxMQ@@._V1_SX300.jpg", + }, + { + id: 137, + title: "Spotlight", + year: "2015", + runtime: "128", + genres: ["Biography", "Crime", "Drama"], + director: "Tom McCarthy", + actors: "Mark Ruffalo, Michael Keaton, Rachel McAdams, Liev Schreiber", + plot: "The true story of how the Boston Globe uncovered the massive scandal of child molestation and cover-up within the local Catholic Archdiocese, shaking the entire Catholic Church to its core.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMjIyOTM5OTIzNV5BMl5BanBnXkFtZTgwMDkzODE2NjE@._V1_SX300.jpg", + }, + { + id: 138, + title: "Vertigo", + year: "1958", + runtime: "128", + genres: ["Mystery", "Romance", "Thriller"], + director: "Alfred Hitchcock", + actors: "James Stewart, Kim Novak, Barbara Bel Geddes, Tom Helmore", + plot: "A San Francisco detective suffering from acrophobia investigates the strange activities of an old friend's wife, all the while becoming dangerously obsessed with her.", + posterUrl: + "http://ia.media-imdb.com/images/M/MV5BNzY0NzQyNzQzOF5BMl5BanBnXkFtZTcwMTgwNTk4OQ@@._V1_SX300.jpg", + }, + { + id: 139, + title: "Whiplash", + year: "2014", + runtime: "107", + genres: ["Drama", "Music"], + director: "Damien Chazelle", + actors: "Miles Teller, J.K. Simmons, Paul Reiser, Melissa Benoist", + plot: "A promising young drummer enrolls at a cut-throat music conservatory where his dreams of greatness are mentored by an instructor who will stop at nothing to realize a student's potential.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTU4OTQ3MDUyMV5BMl5BanBnXkFtZTgwOTA2MjU0MjE@._V1_SX300.jpg", + }, + { + id: 140, + title: "The Lives of Others", + year: "2006", + runtime: "137", + genres: ["Drama", "Thriller"], + director: "Florian Henckel von Donnersmarck", + actors: "Martina Gedeck, Ulrich Mühe, Sebastian Koch, Ulrich Tukur", + plot: "In 1984 East Berlin, an agent of the secret police, conducting surveillance on a writer and his lover, finds himself becoming increasingly absorbed by their lives.", + posterUrl: + "http://ia.media-imdb.com/images/M/MV5BNDUzNjYwNDYyNl5BMl5BanBnXkFtZTcwNjU3ODQ0MQ@@._V1_SX300.jpg", + }, + { + id: 141, + title: "Hotel Rwanda", + year: "2004", + runtime: "121", + genres: ["Drama", "History", "War"], + director: "Terry George", + actors: "Xolani Mali, Don Cheadle, Desmond Dube, Hakeem Kae-Kazim", + plot: "Paul Rusesabagina was a hotel manager who housed over a thousand Tutsi refugees during their struggle against the Hutu militia in Rwanda.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTI2MzQyNTc1M15BMl5BanBnXkFtZTYwMjExNjc3._V1_SX300.jpg", + }, + { + id: 142, + title: "The Martian", + year: "2015", + runtime: "144", + genres: ["Adventure", "Drama", "Sci-Fi"], + director: "Ridley Scott", + actors: "Matt Damon, Jessica Chastain, Kristen Wiig, Jeff Daniels", + plot: "An astronaut becomes stranded on Mars after his team assume him dead, and must rely on his ingenuity to find a way to signal to Earth that he is alive.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMTc2MTQ3MDA1Nl5BMl5BanBnXkFtZTgwODA3OTI4NjE@._V1_SX300.jpg", + }, + { + id: 143, + title: "To Kill a Mockingbird", + year: "1962", + runtime: "129", + genres: ["Crime", "Drama"], + director: "Robert Mulligan", + actors: "Gregory Peck, John Megna, Frank Overton, Rosemary Murphy", + plot: "Atticus Finch, a lawyer in the Depression-era South, defends a black man against an undeserved rape charge, and his kids against prejudice.", + posterUrl: + "http://ia.media-imdb.com/images/M/MV5BMjA4MzI1NDY2Nl5BMl5BanBnXkFtZTcwMTcyODc5Mw@@._V1_SX300.jpg", + }, + { + id: 144, + title: "The Hateful Eight", + year: "2015", + runtime: "187", + genres: ["Crime", "Drama", "Mystery"], + director: "Quentin Tarantino", + actors: + "Samuel L. Jackson, Kurt Russell, Jennifer Jason Leigh, Walton Goggins", + plot: "In the dead of a Wyoming winter, a bounty hunter and his prisoner find shelter in a cabin currently inhabited by a collection of nefarious characters.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BMjA1MTc1NTg5NV5BMl5BanBnXkFtZTgwOTM2MDEzNzE@._V1_SX300.jpg", + }, + { + id: 145, + title: "A Separation", + year: "2011", + runtime: "123", + genres: ["Drama", "Mystery"], + director: "Asghar Farhadi", + actors: "Peyman Moaadi, Leila Hatami, Sareh Bayat, Shahab Hosseini", + plot: "A married couple are faced with a difficult decision - to improve the life of their child by moving to another country or to stay in Iran and look after a deteriorating parent who has Alzheimer's disease.", + posterUrl: + "http://ia.media-imdb.com/images/M/MV5BMTYzMzU4NDUwOF5BMl5BanBnXkFtZTcwMTM5MjA5Ng@@._V1_SX300.jpg", + }, + { + id: 146, + title: "The Big Short", + year: "2015", + runtime: "130", + genres: ["Biography", "Comedy", "Drama"], + director: "Adam McKay", + actors: "Ryan Gosling, Rudy Eisenzopf, Casey Groves, Charlie Talbert", + plot: "Four denizens in the world of high-finance predict the credit and housing bubble collapse of the mid-2000s, and decide to take on the big banks for their greed and lack of foresight.", + posterUrl: + "https://images-na.ssl-images-amazon.com/images/M/MV5BNDc4MThhN2EtZjMzNC00ZDJmLThiZTgtNThlY2UxZWMzNjdkXkEyXkFqcGdeQXVyNDk3NzU2MTQ@._V1_SX300.jpg", + }, +]; diff --git a/ui-next/src/pages/queueMonitor/PollDataTable.tsx b/ui-next/src/pages/queueMonitor/PollDataTable.tsx new file mode 100644 index 0000000000..4d5555b3e1 --- /dev/null +++ b/ui-next/src/pages/queueMonitor/PollDataTable.tsx @@ -0,0 +1,205 @@ +import { Box, Grid, Radio } from "@mui/material"; +import { useActor, useSelector } from "@xstate/react"; +import { DataTable } from "components"; +import { first, last, omit, path } from "lodash/fp"; +import { useContext } from "react"; +import { Entries } from "types/helperTypes"; +import useCustomPagination from "utils/hooks/useCustomPagination"; +import { State } from "xstate"; +import { QuickSearchRefresh } from "./QuickSearchAndRefresh"; +import { FilterSection } from "./filter"; +import { lastPollTimeColumnRenderer } from "./helpers"; +import { + PollData, + QueueMachineEventTypes, + QueueMonitorContext, + QueueMonitorMachineContext, +} from "./state"; +import { QueueData, QueueSizeCount } from "./state/types"; + +const dataColumns: any = [ + { + name: "queueName", + id: "queueName", + label: "Queue Name", + tooltip: "The name of the queue", + }, + { + name: "size", + id: "size", + label: "Queue Size", + maxWidth: "200px", + tooltip: "The number of items in the queue", + }, + { + name: "pollerCount", + id: "pollerCount", + label: "Worker Count", + tooltip: "The number of workers polling the queue", + maxWidth: "200px", + }, + { + name: "lastPollTime", + id: "lastPollTime", + label: "Last Poll Time", + tooltip: "The last time the queue was polled", + renderer: lastPollTimeColumnRenderer, + }, +]; + +type SelectablePollDataSummary = Partial & { + size: number; + pollerCount: number; +}; + +export const PollDataTable = () => { + const { queueMachineActor } = useContext(QueueMonitorContext); + const [ + { pageParam, searchParam }, + { handleSearchTermChange, handlePageChange }, + ] = useCustomPagination(); + + const data: any = useSelector( + queueMachineActor!, + ({ + context: { pollDataByQueueName = {}, queueData = {} }, + }: State) => { + const [usedKeys, activeWorkers] = ( + Object.entries(pollDataByQueueName) as unknown as Array< + [string, PollData[]] + > + ).reduce( + ( + acc: [string[], SelectablePollDataSummary[]], + [itemName, pollData]: [string, PollData[]], + ): [string[], SelectablePollDataSummary[]] => { + const { size = 0, pollerCount = 0 } = path(itemName, queueData) || {}; + + const lastUpdatedPollDataBetweenWorkers = + first(pollData.sort((pd) => pd.lastPollTime)) || {}; + + const usedKeysAcc = (first(acc) as string[]).concat(itemName); + + const selectablePollData: SelectablePollDataSummary = { + ...lastUpdatedPollDataBetweenWorkers, + ...{ size, pollerCount }, + }; + + const activeWorkeresAcc = ( + last(acc) as SelectablePollDataSummary[] + ).concat(selectablePollData); + + return [usedKeysAcc, activeWorkeresAcc]; + }, + [[], []], + ); + + const queueDataWithoutPollData: QueueData = omit( + usedKeys, + queueData, + ) as QueueData; + const inactiveWorkers = ( + Object.entries(queueDataWithoutPollData) as Entries + ).map( + // @ts-ignore + ([k, val = {}]: [ + string, + QueueSizeCount, + ]): SelectablePollDataSummary => ({ + queueName: k!, + pollerCount: 0, + ...val, + }), + ); + return activeWorkers.concat(inactiveWorkers); + }, + ); + + const selectedQueueName = useSelector( + queueMachineActor!, + (state) => state.context.selectedQueueName, + ); + const [, send] = useActor(queueMachineActor!); + const handleSelectRow = (queueName: string) => { + send({ + type: QueueMachineEventTypes.SELECT_QUEUE_NAME, + queueName, + }); + }; + + const selectColumn = { + name: "select", + id: "select", + label: "Select", + maxWidth: "150px", + tooltip: "Select the queue", + renderer: (_id: any, rowData: SelectablePollDataSummary) => { + return ( + { + handleSelectRow(rowData.queueName!); + }} + checked={rowData.queueName === selectedQueueName} + /> + ); + }, + sortFunction: ( + rowA: SelectablePollDataSummary, + rowB: SelectablePollDataSummary, + ) => { + if (rowA.queueName === selectedQueueName) { + return 1; + } + + if (rowB.queueName === selectedQueueName) { + return -1; + } + + return 0; + }, + }; + + const columns: any = [selectColumn].concat(dataColumns); + + const isLoading = useSelector( + queueMachineActor!, + (state) => !state.matches("ready"), + ); + + return ( + + + + + + + + null} + localStorageKey="pollDataTable" + noDataComponent={ + + No polling details found + + } + defaultShowColumns={["select", "queueName", "size", "pollerCount"]} + data={data} + columns={columns} + searchTerm={searchParam} + onChangePage={handlePageChange} + paginationDefaultPage={pageParam ? Number(pageParam) : 1} + /> + + + + ); +}; diff --git a/ui-next/src/pages/queueMonitor/PollWorkerDetails.tsx b/ui-next/src/pages/queueMonitor/PollWorkerDetails.tsx new file mode 100644 index 0000000000..fb1316d7e2 --- /dev/null +++ b/ui-next/src/pages/queueMonitor/PollWorkerDetails.tsx @@ -0,0 +1,66 @@ +import { Box } from "@mui/material"; +import { useSelector } from "@xstate/react"; +import { DataTable } from "components"; +import _path from "lodash/fp/path"; +import { useContext, useEffect, useRef } from "react"; +import { lastPollTimeColumnRenderer } from "./helpers"; +import { QueueMonitorContext } from "./state"; + +const columns = [ + { + id: "workerId", + name: "workerId", + label: "Worker", + }, + { + id: "domain", + name: "domain", + label: "Domain", + }, + { + id: "lastPollTime", + name: "lastPollTime", + label: "Last Poll Time", + renderer: lastPollTimeColumnRenderer, + }, +]; +export const PollWorkerDetailsDataTable = () => { + const { queueMachineActor } = useContext(QueueMonitorContext); + const divRef = useRef(null); + const [selectedName, noWorkers] = useSelector(queueMachineActor!, (state) => [ + state.context.selectedQueueName, + state.context.noWorkers, + ]); + const data = useSelector(queueMachineActor!, (state) => + _path(state.context.selectedQueueName, state.context.pollDataByQueueName), + ); + useEffect(() => { + if (divRef?.current !== null) { + divRef.current.scrollIntoView({ behavior: "smooth" }); + } + }, [selectedName]); + return ( +
    + {noWorkers ? ( + + There are no polling workers + + ) : ( + + Details not found + + } + data={data} + columns={columns} + /> + )} +
    + ); +}; diff --git a/ui-next/src/pages/queueMonitor/QuickSearchAndRefresh.tsx b/ui-next/src/pages/queueMonitor/QuickSearchAndRefresh.tsx new file mode 100644 index 0000000000..3ecd7616e9 --- /dev/null +++ b/ui-next/src/pages/queueMonitor/QuickSearchAndRefresh.tsx @@ -0,0 +1,56 @@ +import { Box, Grid, useMediaQuery, useTheme } from "@mui/material"; +import ConductorInput from "components/v1/ConductorInput"; +import { ReactNode } from "react"; +import { RefreshOptions } from "./refresher"; + +export interface QuickSearchProps { + onChange: (val: string) => void; + searchTerm: string; + createButton?: ReactNode; + description?: ReactNode; + quickSearchPlaceholder: string; + label?: ReactNode; +} + +export const QuickSearchRefresh = ({ + label, + quickSearchPlaceholder, + searchTerm, + onChange, +}: QuickSearchProps) => { + const theme = useTheme(); + const mediumScreen = useMediaQuery(theme.breakpoints.up("md")); + + return ( + + + + + + + + + + + ); +}; diff --git a/ui-next/src/pages/queueMonitor/TaskQueue.tsx b/ui-next/src/pages/queueMonitor/TaskQueue.tsx new file mode 100644 index 0000000000..aefc42406f --- /dev/null +++ b/ui-next/src/pages/queueMonitor/TaskQueue.tsx @@ -0,0 +1,59 @@ +import { Box, Button } from "@mui/material"; +import { useSelector } from "@xstate/react"; +import Paper from "components/Paper"; +import { Helmet } from "react-helmet"; +import { useNavigate } from "react-router"; +import SectionContainer from "shared/SectionContainer"; +import SectionHeader from "shared/SectionHeader"; +import { PollDataTable } from "./PollDataTable"; +import { PollWorkerDetailsDataTable } from "./PollWorkerDetails"; +import { QueueMonitorContextProvider, useQueueMachine } from "./state"; + +export default function TaskQueue() { + const queueMachineActor = useQueueMachine(); + const hasMadeSelection = useSelector(queueMachineActor, (state) => + state.matches("ready.tableSelection.withSelection"), + ); + const showError = useSelector(queueMachineActor, (state) => + state.matches("showError"), + ); + const errorMessage = useSelector( + queueMachineActor, + (state) => state.context.errorMessage, + ); + + const navigate = useNavigate(); + return ( + <> + + Task Queues Monitoring + + + + + {showError ? ( + + {errorMessage} + + + ) : ( + + + {queueMachineActor && } + {hasMadeSelection && } + + + )} + + + ); +} diff --git a/ui-next/src/pages/queueMonitor/filter/FilterSection.tsx b/ui-next/src/pages/queueMonitor/filter/FilterSection.tsx new file mode 100644 index 0000000000..e4cb1506b6 --- /dev/null +++ b/ui-next/src/pages/queueMonitor/filter/FilterSection.tsx @@ -0,0 +1,229 @@ +import { Grid, MenuItem, Paper, useMediaQuery } from "@mui/material"; +import Button from "components/MuiButton"; +import MuiTypography from "components/MuiTypography"; +import ConductorInput from "components/v1/ConductorInput"; +import ConductorSelect from "components/v1/ConductorSelect"; +import ConductorDateTimePicker from "components/v1/date-time/ConductorDateTimePicker"; +import FilterIcon from "components/v1/icons/FilterIcon"; +import ResetIcon from "components/v1/icons/ResetIcon"; +import _isEmpty from "lodash/isEmpty"; +import { ChangeEvent, FunctionComponent, ReactNode } from "react"; +import { Link as RouterLink } from "react-router"; +import { dateRangePickerStyle } from "shared/styles"; +import { ActorRef } from "xstate"; +import { + FilterOption, + QueueMonitorMachineEvents, + RangeOptions, +} from "../state"; +import { useFilterUpdate } from "./hook"; + +interface OptionSelectorProps { + onChange: (payload?: FilterOption) => void; + value?: FilterOption; + label: string; +} + +export const OptionSelector: FunctionComponent = ({ + onChange, + value, + label, +}) => { + const handleSelectChange = (event: ChangeEvent) => { + const maybeSelectedOption = event.target.value as RangeOptions; + + onChange( + _isEmpty(maybeSelectedOption) + ? undefined + : { size: value?.size || 0, option: maybeSelectedOption }, + ); + }; + + return ( + + Greater than + Lower than + Empty + + ); +}; + +interface FilterContainerProps { + label: string; + selector: ReactNode; + valueField: ReactNode; +} + +const FieldContainer: FunctionComponent = ({ + label, + selector, + valueField, +}) => ( + + + + {label} + + + {selector} + {valueField} + +); + +export interface FilterSectionProps { + queueMachineActor: ActorRef; +} + +export const FilterSection: FunctionComponent = ({ + queueMachineActor, +}) => { + const [ + state, + { + handleUpdateQueue, + handleUpdateWorkerCount, + handleUpdateLastPollFilter, + clearAllFields, + }, + isDisabled, + appliedFilterPath, + ] = useFilterUpdate(queueMachineActor); + + const mediumScreen = useMediaQuery("(max-width:1200px)"); + + return ( + + + {/* First row: Queue size and Worker count */} + + + + } + valueField={ + + handleUpdateQueue({ + option: state?.queue?.option || RangeOptions.LT, + size: value, + }) + } + type="number" + /> + } + /> + + + + } + valueField={ + + handleUpdateWorkerCount({ + option: state?.worker?.option || RangeOptions.LT, + size: value, + }) + } + type="number" + /> + } + /> + + + + {/* Second row: Last poll time with Reset and Apply filter buttons */} + + + + } + valueField={ + { + if (value) { + handleUpdateLastPollFilter({ + option: state?.lastPollTime?.option || RangeOptions.LT, + size: value.valueOf(), + }); + } + }} + sx={dateRangePickerStyle.input} + /> + } + /> + + + + + + + + + ); +}; diff --git a/ui-next/src/pages/queueMonitor/filter/hook.ts b/ui-next/src/pages/queueMonitor/filter/hook.ts new file mode 100644 index 0000000000..bc870eb5cc --- /dev/null +++ b/ui-next/src/pages/queueMonitor/filter/hook.ts @@ -0,0 +1,88 @@ +import { + FilterOption, + FilterOptions, + RangeOptions, + QueueMonitorMachineEvents, + QueueMachineEventTypes, +} from "../state"; +import { filterOptionToQueryParams, hasNoQueryParams } from "../helpers"; +import { useLocation } from "react-router"; +import { ActorRef } from "xstate"; +import { useSelector, useActor } from "@xstate/react"; +import fastDeepEquals from "fast-deep-equal"; + +export enum FormReducerActionTypes { + UPDATE_QUEUE_OPTION = "UPDATE_QUEUE_OPTION", + UPDATE_WORKER_COUNT_OPTION = "UPDATE_WORKER_OPTION", + UPDATE_LAST_POLL_TIME_OPTION = "UPDATE_LAST_POLL_TIME_OPTION", +} + +type Payload = + | { + option: RangeOptions; + size: number; + } + | undefined; + +export interface ReducerAction { + type: FormReducerActionTypes; + payload: Payload; +} + +export const useFilterUpdate = ( + queueMachineActor: ActorRef, +): [FilterOptions, any, boolean, string] => { + const location = useLocation(); + const [, send] = useActor(queueMachineActor); + const filterOptions = useSelector( + queueMachineActor, + (state) => state.context.filterOptionsToApply, + ); + + const originalFilterOptions = useSelector( + queueMachineActor, + (state) => state.context.filterOptions, + ); + + const queryParams = filterOptionToQueryParams(filterOptions); + + const handleUpdateQueue = (queue: FilterOption | undefined) => + send({ + type: QueueMachineEventTypes.UPDATE_QUEUE_OPTION, + queue, + }); + + const handleUpdateWorkerCount = (worker: FilterOption | undefined) => + send({ + type: QueueMachineEventTypes.UPDATE_WORKER_COUNT_OPTION, + worker, + }); + + const handleUpdateLastPollFilter = (lastPollTime: FilterOption | undefined) => + send({ + type: QueueMachineEventTypes.UPDATE_LAST_POLL_TIME_OPTION, + lastPollTime, + }); + + const isDisabled = fastDeepEquals(originalFilterOptions, filterOptions); + + const clearAllFields = () => { + handleUpdateQueue(undefined); + handleUpdateWorkerCount(undefined); + handleUpdateLastPollFilter(undefined); + }; + + return [ + filterOptions, + { + handleUpdateQueue, + handleUpdateWorkerCount, + handleUpdateLastPollFilter, + clearAllFields, + }, + isDisabled, + hasNoQueryParams(filterOptions) + ? location.pathname + : `${location.pathname}?${queryParams}`, + ]; +}; diff --git a/ui-next/src/pages/queueMonitor/filter/index.ts b/ui-next/src/pages/queueMonitor/filter/index.ts new file mode 100644 index 0000000000..161f97c6f0 --- /dev/null +++ b/ui-next/src/pages/queueMonitor/filter/index.ts @@ -0,0 +1,3 @@ +import { FilterSection } from "./FilterSection"; + +export { FilterSection }; diff --git a/ui-next/src/pages/queueMonitor/helpers.ts b/ui-next/src/pages/queueMonitor/helpers.ts new file mode 100644 index 0000000000..2a8cd6a5ca --- /dev/null +++ b/ui-next/src/pages/queueMonitor/helpers.ts @@ -0,0 +1,70 @@ +import _path from "lodash/fp/path"; +import _isNil from "lodash/isNil"; +import _isUndefined from "lodash/isUndefined"; +import { Entries } from "types/helperTypes"; +import { getDifferenceInSeconds, humanizeDuration } from "utils/date"; +import { FilterOption, FilterOptions, RangeOptions } from "./state/types"; + +interface QueueMonitorRoute { + workerSize?: string; + workerOpt?: RangeOptions; + queueSize?: string; + queueOpt?: RangeOptions; + lastPollTimeSize?: string; + lastPollTimeOpt?: RangeOptions; +} + +export const filterOptionOrNot = ( + prefix: string, + matchParams: QueueMonitorRoute, +): FilterOption | undefined => { + const size = _path(`${prefix}Size`, matchParams); + const option = _path(`${prefix}Opt`, matchParams); + return [size, option].every(_isNil) + ? undefined + : { + size, + option, + }; +}; +export const renameKeys = ( + someObj: Record, + newNames: Record, +): Record => + Object.fromEntries( + Object.entries(someObj).map(([key, value]) => [ + _path(key, newNames), + value, + ]), + ); + +export const filterOptionToQueryParams = ( + filterOptions: FilterOptions, +): string => + (Object.entries(filterOptions) as Entries>) + .reduce((acc: string[], [key, value]): string[] => { + if (_isNil(value)) { + return acc; + } + let size = value.size || 0; + if (key === "lastPollTime") { + size = size || Date.now(); + } + return acc.concat(`${key}Size=${size}&${key}Opt=${value?.option}`); + }, []) + .join("&"); + +export const hasNoQueryParams = (filterOptions: FilterOptions) => + Object.values(filterOptions).every(_isUndefined); + +export const lastPollTimeColumnRenderer = (lastPollTime: number) => { + if (lastPollTime) { + const now = Date.now(); + const durationInMillis = now - lastPollTime; + const secondsDiff = getDifferenceInSeconds(now, lastPollTime); + return secondsDiff > 0 + ? humanizeDuration(lastPollTime, now) + : `${Math.abs(durationInMillis)} millis`; + } + return "N/A"; +}; diff --git a/ui-next/src/pages/queueMonitor/refresher/RefreshOptions.tsx b/ui-next/src/pages/queueMonitor/refresher/RefreshOptions.tsx new file mode 100644 index 0000000000..7a3594d894 --- /dev/null +++ b/ui-next/src/pages/queueMonitor/refresher/RefreshOptions.tsx @@ -0,0 +1,181 @@ +import { + CircularProgress, + FormControlLabel, + Grid, + Radio, + RadioGroup, +} from "@mui/material"; +import { ArrowClockwise as RefreshIcon } from "@phosphor-icons/react"; +import { useActor, useSelector } from "@xstate/react"; +import Button from "components/MuiButton"; +import MuiTypography from "components/MuiTypography"; +import { FunctionComponent, ReactNode, useContext, useMemo } from "react"; +import { ActorRef, State } from "xstate"; +import { QueueMonitorContext } from "../state"; +import { + RefreshMachineContext, + RefreshMachineEventTypes, + TimerEvents, +} from "./state"; + +const REFRESH_SECONDS_OPTIONS = [1, 10, 30, 60]; + +interface RefreshOptionsPresentationalProps { + onRefresh: () => void; + timerActor: ActorRef; + startIcon: ReactNode; +} + +export const RefreshButton: FunctionComponent< + RefreshOptionsPresentationalProps +> = ({ onRefresh, timerActor, startIcon }) => { + const refreshInterval = useSelector( + timerActor, + (state: State) => state.context.durationSet, + ); + + const elapsed = useSelector( + timerActor, + (state: State) => state.context.elapsed, + ); + + return ( + + ); +}; + +export const RefreshOptions = () => { + const { queueMachineActor } = useContext(QueueMonitorContext); + + const [, send] = useActor(queueMachineActor!); + + const canRefresh = useSelector(queueMachineActor!, (state) => + state.matches("ready.refresher.timer"), + ); + + const timerActor = + // @ts-ignore + queueMachineActor?.children?.get("refreshMachine"); + + const refreshInterval = useSelector( + queueMachineActor!, + (state) => state.context.refetchDuration, + ); + + const changeRefreshRate = (value: number) => { + send({ + type: RefreshMachineEventTypes.UPDATE_DURATION, + value, + }); + }; + const handleRefresh = () => + send({ + type: RefreshMachineEventTypes.REFRESH, + }); + + const startIcon = useMemo(() => { + return refreshInterval === 1 ? ( + + ) : ( + + ); + }, [refreshInterval]); + + const refreshButton = + canRefresh && timerActor ? ( + + ) : ( + + ); + + const radioGroup = ( + + {REFRESH_SECONDS_OPTIONS.map((op) => ( + changeRefreshRate(op)} + checked={op === refreshInterval} + /> + } + label={op} + key={op} + /> + ))} + + ); + + const label = ( + + Refresh seconds + + ); + + return ( + + + {label} + + + {label} + + + {radioGroup} + + + + {refreshButton} + + + + {radioGroup} + {refreshButton} + + + {radioGroup} + + {refreshButton} + + ); +}; diff --git a/ui-next/src/pages/queueMonitor/refresher/index.ts b/ui-next/src/pages/queueMonitor/refresher/index.ts new file mode 100644 index 0000000000..6c64f1c36f --- /dev/null +++ b/ui-next/src/pages/queueMonitor/refresher/index.ts @@ -0,0 +1,2 @@ +export * from "./RefreshOptions"; +export * from "./state"; diff --git a/ui-next/src/pages/queueMonitor/refresher/state/actions.ts b/ui-next/src/pages/queueMonitor/refresher/state/actions.ts new file mode 100644 index 0000000000..a6fdd61e75 --- /dev/null +++ b/ui-next/src/pages/queueMonitor/refresher/state/actions.ts @@ -0,0 +1,29 @@ +import { assign, sendParent } from "xstate"; +import { + RefreshMachineContext, + UpdateDurationEvent, + RefreshMachineEventTypes, +} from "./types"; + +export const persistDuration = assign< + RefreshMachineContext, + UpdateDurationEvent +>({ + duration: (_, event) => event.value, + durationSet: (_, event) => event.value, +}); + +export const persistElapsed = assign({ + elapsed: (context) => context.elapsed + 1, +}); + +export const sendRefresh = sendParent(() => ({ + type: RefreshMachineEventTypes.REFRESH, +})); + +export const forwardToParent = sendParent((__context, event) => event); + +export const restartTimer = assign({ + duration: ({ durationSet }) => durationSet, + elapsed: 0, +}); diff --git a/ui-next/src/pages/queueMonitor/refresher/state/guards.ts b/ui-next/src/pages/queueMonitor/refresher/state/guards.ts new file mode 100644 index 0000000000..e4dfaf0c0c --- /dev/null +++ b/ui-next/src/pages/queueMonitor/refresher/state/guards.ts @@ -0,0 +1,8 @@ +import { RefreshMachineContext } from "./types"; + +export const elapsedIsLessThanDuration = (context: RefreshMachineContext) => + context.elapsed < context.duration; + +export const elapsedIsBiggerThanDuration = (context: RefreshMachineContext) => { + return context.elapsed >= context.duration; +}; diff --git a/ui-next/src/pages/queueMonitor/refresher/state/index.ts b/ui-next/src/pages/queueMonitor/refresher/state/index.ts new file mode 100644 index 0000000000..79fd82215f --- /dev/null +++ b/ui-next/src/pages/queueMonitor/refresher/state/index.ts @@ -0,0 +1,2 @@ +export * from "./machine"; +export * from "./types"; diff --git a/ui-next/src/pages/queueMonitor/refresher/state/machine.ts b/ui-next/src/pages/queueMonitor/refresher/state/machine.ts new file mode 100644 index 0000000000..bbe6e49431 --- /dev/null +++ b/ui-next/src/pages/queueMonitor/refresher/state/machine.ts @@ -0,0 +1,60 @@ +import { createMachine } from "xstate"; +import { + RefreshMachineContext, + RefreshMachineEventTypes, + TimerEvents, +} from "./types"; +import * as actions from "./actions"; +import * as guards from "./guards"; + +export const timerMachine = createMachine( + { + id: "timerMachine", + initial: "running", + context: { + durationSet: 60, + elapsed: 0, + duration: 60, + }, + states: { + running: { + invoke: { + src: (_context) => (cb) => { + const interval = setInterval(() => { + cb(RefreshMachineEventTypes.TICK); + }, 1000); + + return () => { + clearInterval(interval); + }; + }, + }, + always: { + target: "endTimer", + cond: "elapsedIsBiggerThanDuration", + }, + on: { + TICK: { + actions: "persistElapsed", + }, + }, + }, + endTimer: { + entry: ["sendRefresh", "restartTimer"], + always: "running", + }, + }, + on: { + [RefreshMachineEventTypes.UPDATE_DURATION]: { + actions: ["persistDuration", "restartTimer"], + }, + [RefreshMachineEventTypes.REFRESH]: { + actions: ["restartTimer"], + }, + }, + }, + { + actions: actions as any, + guards: guards as any, + }, +); diff --git a/ui-next/src/pages/queueMonitor/refresher/state/types.ts b/ui-next/src/pages/queueMonitor/refresher/state/types.ts new file mode 100644 index 0000000000..f0c4ac801a --- /dev/null +++ b/ui-next/src/pages/queueMonitor/refresher/state/types.ts @@ -0,0 +1,26 @@ +export interface RefreshMachineContext { + elapsed: number; + duration: number; + durationSet: number; +} + +export enum RefreshMachineEventTypes { + TICK = "TICK", + UPDATE_DURATION = "UPDATE_DURATION", + REFRESH = "REFRESH", +} + +export type TickEvent = { + type: RefreshMachineEventTypes.TICK; +}; + +export type RefreshEvent = { + type: RefreshMachineEventTypes.REFRESH; +}; + +export type UpdateDurationEvent = { + type: RefreshMachineEventTypes.UPDATE_DURATION; + value: number; +}; + +export type TimerEvents = TickEvent | UpdateDurationEvent | RefreshEvent; diff --git a/ui-next/src/pages/queueMonitor/state/QueueMonitorContext/QueueMonitorContext.tsx b/ui-next/src/pages/queueMonitor/state/QueueMonitorContext/QueueMonitorContext.tsx new file mode 100644 index 0000000000..9032eefe59 --- /dev/null +++ b/ui-next/src/pages/queueMonitor/state/QueueMonitorContext/QueueMonitorContext.tsx @@ -0,0 +1,6 @@ +import { createContext } from "react"; +import { QueueMonitorContextProps } from "./types"; + +export const QueueMonitorContext = createContext({ + queueMachineActor: undefined, +}); diff --git a/ui-next/src/pages/queueMonitor/state/QueueMonitorContext/QueueMonitorProvider.tsx b/ui-next/src/pages/queueMonitor/state/QueueMonitorContext/QueueMonitorProvider.tsx new file mode 100644 index 0000000000..15d281874f --- /dev/null +++ b/ui-next/src/pages/queueMonitor/state/QueueMonitorContext/QueueMonitorProvider.tsx @@ -0,0 +1,11 @@ +import { FunctionComponent } from "react"; +import { QueueMonitorContext } from "./QueueMonitorContext"; +import { QueueMonitorContextProps } from "./types"; + +export const QueueMonitorContextProvider: FunctionComponent< + QueueMonitorContextProps +> = ({ children, queueMachineActor }) => ( + + {children} + +); diff --git a/ui-next/src/pages/queueMonitor/state/QueueMonitorContext/index.ts b/ui-next/src/pages/queueMonitor/state/QueueMonitorContext/index.ts new file mode 100644 index 0000000000..8ba9877e07 --- /dev/null +++ b/ui-next/src/pages/queueMonitor/state/QueueMonitorContext/index.ts @@ -0,0 +1,2 @@ +export * from "./QueueMonitorContext"; +export * from "./QueueMonitorProvider"; diff --git a/ui-next/src/pages/queueMonitor/state/QueueMonitorContext/types.ts b/ui-next/src/pages/queueMonitor/state/QueueMonitorContext/types.ts new file mode 100644 index 0000000000..7cbe08d02b --- /dev/null +++ b/ui-next/src/pages/queueMonitor/state/QueueMonitorContext/types.ts @@ -0,0 +1,8 @@ +import { ReactNode } from "react"; +import { ActorRef } from "xstate"; +import { QueueMonitorMachineEvents } from "../types"; + +export interface QueueMonitorContextProps { + queueMachineActor?: ActorRef; + children?: ReactNode; +} diff --git a/ui-next/src/pages/queueMonitor/state/actions.ts b/ui-next/src/pages/queueMonitor/state/actions.ts new file mode 100644 index 0000000000..94443fc4e7 --- /dev/null +++ b/ui-next/src/pages/queueMonitor/state/actions.ts @@ -0,0 +1,97 @@ +import { assign, DoneInvokeEvent, forwardTo } from "xstate"; +import { + QueueMonitorMachineContext, + FetchQueueEvent, + PollData, + FetchResponse, + SelectQueueEvent, + UpdateQueueOptionEvent, + UpdateWorkerOptionEvent, + UpdateLastPollTimeOptionEvent, +} from "./types"; +import { UpdateDurationEvent } from "../refresher"; +import _groupBy from "lodash/groupBy"; +import _isNil from "lodash/isNil"; +import _path from "lodash/fp/path"; + +export const persistFetchRequestParams = assign< + QueueMonitorMachineContext, + FetchQueueEvent +>((_context, { type: _type, ...rest }) => { + return { + filterOptions: rest, + filterOptionsToApply: rest, + }; +}); + +export const persistPollQueueData = assign< + QueueMonitorMachineContext, + DoneInvokeEvent +>((_context, { data }) => ({ + pollDataByQueueName: _groupBy( + data.pollData, + "queueName", + ) as unknown as Record, + queueData: data.queueData, +})); + +export const persistQueueSelection = assign< + QueueMonitorMachineContext, + SelectQueueEvent +>((context, { queueName }) => ({ + selectedQueueName: queueName, + noWorkers: _isNil(_path(queueName, context.pollDataByQueueName)), +})); + +export const persistQueueOption = assign< + QueueMonitorMachineContext, + UpdateQueueOptionEvent +>({ + filterOptionsToApply: ({ filterOptionsToApply }, { queue }) => ({ + ...filterOptionsToApply, + queue, + }), +}); + +export const persistWorkerOption = assign< + QueueMonitorMachineContext, + UpdateWorkerOptionEvent +>({ + filterOptionsToApply: ({ filterOptionsToApply }, { worker }) => ({ + ...filterOptionsToApply, + worker, + }), +}); + +export const persistLastPollTimeOption = assign< + QueueMonitorMachineContext, + UpdateLastPollTimeOptionEvent +>({ + filterOptionsToApply: ({ filterOptionsToApply }, { lastPollTime }) => ({ + ...filterOptionsToApply, + lastPollTime, + }), +}); + +export const peristErrorMessage = assign< + QueueMonitorMachineContext, + DoneInvokeEvent<{ message: string }> +>({ errorMessage: (_context: any, { data }: any) => data.message }); + +export const persistDuration = assign< + QueueMonitorMachineContext, + UpdateDurationEvent +>({ + refetchDuration: (context, { value }) => value, +}); + +export const persistLocalStorageDuration = assign< + QueueMonitorMachineContext, + DoneInvokeEvent +>({ + refetchDuration: (context, { data }) => { + return data; + }, +}); + +export const forwardToRefreshMachine = forwardTo("refreshMachine"); diff --git a/ui-next/src/pages/queueMonitor/state/guards.ts b/ui-next/src/pages/queueMonitor/state/guards.ts new file mode 100644 index 0000000000..522b41e19e --- /dev/null +++ b/ui-next/src/pages/queueMonitor/state/guards.ts @@ -0,0 +1,5 @@ +import { QueueMonitorMachineContext } from "./types"; +import _isNil from "lodash/isNil"; + +export const noQueueNameSelected = (context: QueueMonitorMachineContext) => + _isNil(context.selectedQueueName); diff --git a/ui-next/src/pages/queueMonitor/state/hook.ts b/ui-next/src/pages/queueMonitor/state/hook.ts new file mode 100644 index 0000000000..3b6c81daa1 --- /dev/null +++ b/ui-next/src/pages/queueMonitor/state/hook.ts @@ -0,0 +1,36 @@ +import { useEffect, useMemo } from "react"; +import { useAuthHeaders } from "utils/query"; +import { useMachine } from "@xstate/react"; +import { queueMonitorMachine } from "./machine"; +import { QueueMachineEventTypes, QueueMonitorMachineEvents } from "./types"; +import { ActorRef } from "xstate"; +import { useLocation } from "react-router"; +import qs from "qs"; +import { filterOptionOrNot } from "../helpers"; + +export const useQueueMachine = (): ActorRef => { + const authHeaders = useAuthHeaders(); + const { search } = useLocation(); + const [, send, service] = useMachine(queueMonitorMachine, { + ...(process.env.NODE_ENV === "development" ? { devTools: true } : {}), + context: { + authHeaders, + }, + }); + + const queryParams = useMemo( + () => qs.parse(search, { ignoreQueryPrefix: true }), + [search], + ); + + useEffect(() => { + send({ + type: QueueMachineEventTypes.FETCH_TASKS_QUEUE, + queue: filterOptionOrNot("queue", queryParams), + worker: filterOptionOrNot("worker", queryParams), + lastPollTime: filterOptionOrNot("lastPollTime", queryParams), + }); + }, [send, queryParams]); + + return service; +}; diff --git a/ui-next/src/pages/queueMonitor/state/index.ts b/ui-next/src/pages/queueMonitor/state/index.ts new file mode 100644 index 0000000000..e6cf051b40 --- /dev/null +++ b/ui-next/src/pages/queueMonitor/state/index.ts @@ -0,0 +1,3 @@ +export * from "./hook"; +export * from "./QueueMonitorContext"; +export * from "./types"; diff --git a/ui-next/src/pages/queueMonitor/state/machine.ts b/ui-next/src/pages/queueMonitor/state/machine.ts new file mode 100644 index 0000000000..26b09c54db --- /dev/null +++ b/ui-next/src/pages/queueMonitor/state/machine.ts @@ -0,0 +1,183 @@ +import { createMachine } from "xstate"; +import { timerMachine } from "../refresher/state/machine"; +import { RefreshMachineEventTypes } from "../refresher/state/types"; +import * as actions from "./actions"; +import * as guards from "./guards"; +import * as services from "./service"; +import { + QueueMachineEventTypes, + QueueMonitorMachineContext, + QueueMonitorMachineEvents, +} from "./types"; + +export const queueMonitorMachine = createMachine< + QueueMonitorMachineContext, + QueueMonitorMachineEvents +>( + { + id: "queueMachine", + predictableActionArguments: true, + initial: "idle", + context: { + pollDataByQueueName: {}, + queueData: {}, + authHeaders: undefined, + refetchDuration: 60, + filterOptions: { + queue: undefined, + worker: undefined, + lastPollTime: undefined, + }, + filterOptionsToApply: { + queue: undefined, + worker: undefined, + lastPollTime: undefined, + }, + errorMessage: "", + }, + on: { + [QueueMachineEventTypes.FETCH_TASKS_QUEUE]: { + actions: "persistFetchRequestParams", + target: "fetchForTaskPolls", + }, + [RefreshMachineEventTypes.UPDATE_DURATION]: { + actions: ["persistDuration"], + target: "updateDurationDuringRefresh", + }, + }, + states: { + idle: {}, + showError: {}, + fetchForTaskPolls: { + invoke: { + src: "fetchForPollData", + onDone: { + actions: "persistPollQueueData", + target: "checkRefreshConfig", + }, + onError: { + actions: "peristErrorMessage", + target: "showError", + }, + }, + }, + checkRefreshConfig: { + invoke: { + src: "maybePullOrderAndVisibility", + onDone: { + actions: ["persistLocalStorageDuration"], + target: "ready", + }, + }, + }, + updateDurationDuringRefresh: { + invoke: { + src: "saveOrderAndVisibility", + onDone: "checkRefreshConfig", + }, + }, + ready: { + on: { + [QueueMachineEventTypes.UPDATE_QUEUE_OPTION]: { + actions: "persistQueueOption", + }, + [QueueMachineEventTypes.UPDATE_WORKER_COUNT_OPTION]: { + actions: "persistWorkerOption", + }, + [QueueMachineEventTypes.UPDATE_LAST_POLL_TIME_OPTION]: { + actions: "persistLastPollTimeOption", + }, + }, + type: "parallel", + states: { + tableSelection: { + initial: "checkSelection", + states: { + checkSelection: { + always: [ + { cond: "noQueueNameSelected", target: "noSelection" }, + { + target: "withSelection", + }, + ], + }, + noSelection: { + on: { + [QueueMachineEventTypes.SELECT_QUEUE_NAME]: { + actions: "persistQueueSelection", + target: "withSelection", + }, + }, + }, + withSelection: { + on: { + [QueueMachineEventTypes.SELECT_QUEUE_NAME]: { + actions: "persistQueueSelection", + target: "withSelection", + }, + }, + }, + }, + }, + refresher: { + invoke: { + src: timerMachine, + id: "refreshMachine", + data: ({ refetchDuration }) => ({ + durationSet: refetchDuration, + elapsed: 0, + duration: refetchDuration, + }), + }, + initial: "timer", + states: { + fetchForTaskPolls: { + invoke: { + src: "fetchForPollData", + onDone: { + actions: "persistPollQueueData", + target: "timer", + }, + }, + }, + timer: { + on: { + [RefreshMachineEventTypes.REFRESH]: { + target: "fetchForTaskPolls", + actions: ["forwardToRefreshMachine"], + }, + [RefreshMachineEventTypes.UPDATE_DURATION]: { + actions: ["persistDuration", "forwardToRefreshMachine"], + target: "updateDuration", + }, + }, + }, + updateDuration: { + invoke: { + src: "saveOrderAndVisibility", + onDone: "timer", + }, + }, + }, + }, + filterDialog: { + initial: "closeDialog", + states: { + closeDialog: { + on: { openDialog: "openDialog" }, + }, + openDialog: { + on: { closeDialog: "closeDialog" }, + }, + }, + }, + }, + }, + }, + }, + { + actions: actions as any, + services, + guards: guards as any, + }, +); diff --git a/ui-next/src/pages/queueMonitor/state/service.ts b/ui-next/src/pages/queueMonitor/state/service.ts new file mode 100644 index 0000000000..15f5cb3067 --- /dev/null +++ b/ui-next/src/pages/queueMonitor/state/service.ts @@ -0,0 +1,57 @@ +import { queryClient } from "queryClient"; +import { fetchWithContext, fetchContextNonHook } from "plugins/fetch"; +import { QueueMonitorMachineContext } from "./types"; +import { logger } from "utils/logger"; +import { hasNoQueryParams, filterOptionToQueryParams } from "../helpers"; + +const fetchContext = fetchContextNonHook(); + +const queuePollDataPath = `/tasks/queue/polldata/all`; + +const LOCAL_STORAGE_KEY = "queueMonitorRefreshSeconds"; + +export const fetchForPollData = async ({ + authHeaders: headers, + filterOptions, +}: QueueMonitorMachineContext) => { + const url = hasNoQueryParams(filterOptions) + ? queuePollDataPath + : `${queuePollDataPath}?${filterOptionToQueryParams(filterOptions)}`; + + logger.info("Will hit path to fetch for tasks ", url, filterOptions); + try { + const response = await queryClient.fetchQuery( + [fetchContext.stack, url], + () => fetchWithContext(url, fetchContext, { headers }), + ); + return response; + } catch (error: any) { + logger.error("Fetching task list page", error); + return Promise.reject({ + message: + error.status === 403 + ? "It seems like you do not have permissions to view this page. Please check with your cluster administrator." + : "Error fetching task list page", + }); + } +}; + +export const saveOrderAndVisibility = async ( + context: QueueMonitorMachineContext, +) => { + const { refetchDuration } = context; + window.localStorage.setItem(LOCAL_STORAGE_KEY, `${refetchDuration}`); + + return true; +}; + +export const maybePullOrderAndVisibility = async ( + context: QueueMonitorMachineContext, +) => { + const { refetchDuration } = context; + const savedOrder = window.localStorage.getItem(LOCAL_STORAGE_KEY); + if (savedOrder) { + return parseInt(savedOrder, 10); + } + return refetchDuration; +}; diff --git a/ui-next/src/pages/queueMonitor/state/types.ts b/ui-next/src/pages/queueMonitor/state/types.ts new file mode 100644 index 0000000000..68bd597dcb --- /dev/null +++ b/ui-next/src/pages/queueMonitor/state/types.ts @@ -0,0 +1,89 @@ +import { DoneInvokeEvent } from "xstate"; +import { AuthHeaders } from "types/common"; +import { RefreshEvent, UpdateDurationEvent } from "../refresher/state/types"; + +export type QueueSizeCount = { + size: number; + pollerCount?: number; +}; + +export type QueueData = Record; + +export interface PollData { + queueName: string; + domain: string; + workerId: string; + lastPollTime: number; +} + +export enum RangeOptions { + GT = "GT", + LT = "LT", +} + +export type FilterOption = { + size: number; + option: RangeOptions; +}; + +export interface FilterOptions { + queue?: FilterOption; + worker?: FilterOption; + lastPollTime?: FilterOption; +} + +export interface QueueMonitorMachineContext { + authHeaders?: AuthHeaders; + pollDataByQueueName?: Record; + selectedQueueName?: string; + queueData: QueueData; + filterOptions: FilterOptions; + filterOptionsToApply: FilterOptions; + refetchDuration: number; + errorMessage: string; +} + +export enum QueueMachineEventTypes { + FETCH_TASKS_QUEUE = "FETCH_TASKS_QUEUE", + SELECT_QUEUE_NAME = "SELECT_QUEUE_NAME", + + UPDATE_QUEUE_OPTION = "UPDATE_QUEUE_OPTION", + UPDATE_WORKER_COUNT_OPTION = "UPDATE_WORKER_OPTION", + UPDATE_LAST_POLL_TIME_OPTION = "UPDATE_LAST_POLL_TIME_OPTION", +} + +export type UpdateQueueOptionEvent = { + type: QueueMachineEventTypes.UPDATE_QUEUE_OPTION; + queue?: FilterOption; +}; + +export type UpdateWorkerOptionEvent = { + type: QueueMachineEventTypes.UPDATE_WORKER_COUNT_OPTION; + worker?: FilterOption; +}; + +export type UpdateLastPollTimeOptionEvent = { + type: QueueMachineEventTypes.UPDATE_LAST_POLL_TIME_OPTION; + lastPollTime?: FilterOption; +}; + +export type SelectQueueEvent = { + type: QueueMachineEventTypes.SELECT_QUEUE_NAME; + queueName: string; +}; + +export type FetchQueueEvent = { + type: QueueMachineEventTypes.FETCH_TASKS_QUEUE; +} & FilterOptions; + +export type FetchResponse = { queueData: QueueData; pollData: PollData[] }; + +export type QueueMonitorMachineEvents = + | FetchQueueEvent + | SelectQueueEvent + | RefreshEvent + | UpdateQueueOptionEvent + | UpdateWorkerOptionEvent + | UpdateLastPollTimeOptionEvent + | UpdateDurationEvent + | DoneInvokeEvent; diff --git a/ui-next/src/pages/runWorkflow/IdempotencyForm.tsx b/ui-next/src/pages/runWorkflow/IdempotencyForm.tsx new file mode 100644 index 0000000000..93e2448cc8 --- /dev/null +++ b/ui-next/src/pages/runWorkflow/IdempotencyForm.tsx @@ -0,0 +1,124 @@ +import { FormControlLabel, Grid } from "@mui/material"; +import RadioButtonGroup from "components/RadioButtonGroup"; +import Text from "components/Text"; +import ConductorInput from "components/v1/ConductorInput"; +import { colors } from "theme/tokens/variables"; + +const style = { + labelText: { + position: "relative", + fontSize: "13px", + transform: "none", + fontWeight: 600, + paddingLeft: 0, + marginBottom: "0.3em", + color: colors.black, + }, +}; + +export interface IdempotencyFormProps { + idempotencyValues: { + idempotencyKey?: string; + idempotencyStrategy?: IdempotencyStrategyEnum; + }; + onChange: (data: { + idempotencyKey: string; + idempotencyStrategy?: IdempotencyStrategyEnum; + }) => void; + showStrategyInitially?: boolean; +} + +enum IdempotencyStrategyEnum { + FAIL = "FAIL", + RETURN_EXISTING = "RETURN_EXISTING", + FAIL_ON_RUNNING = "FAIL_ON_RUNNING", +} + +export default function IdempotencyForm({ + idempotencyValues, + onChange, + showStrategyInitially, +}: IdempotencyFormProps) { + const { idempotencyKey, idempotencyStrategy } = idempotencyValues; + return ( + <> + + + onChange({ + idempotencyKey: value, + idempotencyStrategy: idempotencyStrategy, + }) + } + tooltip={{ + title: "Idempotency key", + content: + "Idempotency Key is a user generated key to avoid conflicts with other workflows. Idempotency data is retained for the life of the workflow executions.", + }} + /> + + {(idempotencyKey || showStrategyInitially) && ( + <> + + + Idempotency strategy + + { + onChange({ + idempotencyKey: idempotencyKey ?? "", + idempotencyStrategy: value as IdempotencyStrategyEnum, + }); + }} + /> + } + /> + + + )} + + ); +} diff --git a/ui-next/src/pages/runWorkflow/RunWorkflow.tsx b/ui-next/src/pages/runWorkflow/RunWorkflow.tsx new file mode 100644 index 0000000000..c77e4b7606 --- /dev/null +++ b/ui-next/src/pages/runWorkflow/RunWorkflow.tsx @@ -0,0 +1,737 @@ +import { Box, Grid, Paper, Theme } from "@mui/material"; +import { Button } from "components"; +import MuiAlert from "components/MuiAlert"; +import NavLink from "components/NavLink"; +import { ConductorAutoComplete } from "components/v1"; +import { ConductorCodeBlockInput } from "components/v1/ConductorCodeBlockInput"; +import ConductorInput from "components/v1/ConductorInput"; +import SplitButton from "components/v1/ConductorSplitButton"; +import PlayIcon from "components/v1/icons/PlayIcon"; +import ResetIcon from "components/v1/icons/ResetIcon"; +import XCloseIcon from "components/v1/icons/XCloseIcon"; +import { RunWorkflowHistoryTable } from "pages/definition/RunWorkflow/RunWorkflowHistoryTable"; +import { + IdempotencyStrategyEnum, + IdempotencyValuesProp, +} from "pages/definition/RunWorkflow/state"; +import { useEffect, useMemo, useState } from "react"; +import { Helmet } from "react-helmet"; +import { useLocation, useNavigate } from "react-router"; +import { useQueryState } from "react-router-use-location-state"; +import { editor } from "shared/editor"; +import SectionContainer from "shared/SectionContainer"; +import SectionHeader from "shared/SectionHeader"; +import { useAuth } from "shared/auth"; +import { colors } from "theme/tokens/variables"; +import { logger, tryToJson, useLocalStorage } from "utils/index"; +import { useAction, useWorkflowDefsByVersions } from "utils/query"; +import { v4 as uuidv4 } from "uuid"; +import IdempotencyForm from "./IdempotencyForm"; +import { + BuildQueryOutput, + RunWorkflowApiSearchModal, +} from "./RunWorkflowApiSearchModal"; +import { getTemplateFromInputParams } from "./runWorkflowUtils"; + +type InputParameterType = { + inputParameters: string[]; +}; + +type CommonProperties = { + correlationId: string; + input: object; + taskToDomain?: object; + idempotencyKey?: string; + idempotencyStrategy?: IdempotencyStrategyEnum; +}; + +type RunWorkflowParamType = { + name: string; + version: string; +} & CommonProperties; + +interface LocationState { + state: { + execution?: { + workflowName: string; + workflowVersion: number; + } & CommonProperties; + }; +} + +interface RunWorkflowState { + workflowType: string; + workflowVersion: string | null; + workflowVersions: string[]; + workflowInputParams: string[]; + workflowInputTemplate: string; + taskToDomain: string; + workflowCorrelationId: string; + lastCreatedWorkflowId: string; + idempotencyKey: string; + idempotencyStrategy?: IdempotencyStrategyEnum; +} + +const style = { + root: { + padding: 0, + margin: 0, + color: "rgba(0, 0, 0, 1)", + borderLeft: "solid var(--backgroundLight) 2px", + border: "4px solid green", + }, + monaco: { + padding: "10px", + borderStyle: "solid", + borderColor: (theme: Theme) => + theme.palette?.mode === "dark" ? colors.gray04 : colors.gray11, + borderWidth: "1px", + borderRadius: "4px", + "&:focus-within": { + margin: "-2px", + borderColor: "rgb(73, 105, 228)", + borderStyle: "solid", + borderWidth: "2px", + }, + }, + label: { + display: "block", + marginBottom: "8px", + }, + menuBg: { + background: "none", + color: "black", + }, + listClass: { + fontSize: "14px", + lineHeight: 2.1, + display: "flex", + justifyContent: "flex-start", + paddingLeft: "30px", + color: "#293845", + "&:hover": { + backgroundColor: "var(--backgroundLightest)", + }, + "&.active": { + backgroundColor: "var(--backgroundLightest)", + }, + }, + buttonSpacer: { + paddingTop: "30px", + paddingLeft: "10px", + }, + controls: { + height: "calc(100%)", + overflowY: "auto", + width: "calc(100%)", + overflowX: "hidden", + }, + inputBox: { + "& textarea": { + padding: "0 10px", + }, + "& input": { + padding: "0 10px", + }, + }, + largeInputBox: { + width: "100%", + "& div": { + width: "100%", + }, + "& input": { + width: "100%", + }, + }, + labelText: { + position: "relative", + fontSize: "13px", + transform: "none", + fontWeight: 600, + paddingLeft: 0, + marginBottom: "0.3em", + color: "#767676", + }, + dropDown: { + "& .MuiOutlinedInput-root.MuiInputBase-sizeSmall .MuiAutocomplete-input": { + padding: "2.5px 10px 2.5px", + }, + }, +}; + +const GENERIC_ERROR_MESSAGE = "Error while running workflow."; +const INVALID_DATA_MESSAGE = "Invalid data. Cannot run Workflow."; + +// function getInputAreaLength(workflowInputTemplate: string) { +// return Math.max( +// 6, +// (workflowInputTemplate || "").split(/\r\n|\r|\n/).length + 1 +// ); +// } + +const INITIAL_STATE = { + workflowType: "", + workflowVersion: null, + workflowVersions: [], + workflowInputParams: [], + workflowInputTemplate: "", + taskToDomain: "", + workflowCorrelationId: "", + lastCreatedWorkflowId: "", + idempotencyKey: "", + idempotencyStrategy: IdempotencyStrategyEnum.RETURN_EXISTING, +}; + +export function RunWorkflow() { + const [workflowHistory, setWorkflowHistory] = useLocalStorage( + "workflowHistory", + [], + ); + + const { isTrialExpired } = useAuth(); + + const workflowDefByVersions = useWorkflowDefsByVersions(); + const workflowNames = useMemo( + (): string[] => + workflowDefByVersions + ? Array.from(workflowDefByVersions.get("lookups").keys()) + : [], + [workflowDefByVersions], + ); + + const location: LocationState = useLocation(); + const latestExecution = useMemo(() => location.state?.execution, [location]); + + const [selectedWorkflow, setSelectedWorkflow] = useLocalStorage( + "selectedWorkflow", + {}, + ); + const memorizedState = useMemo(() => { + const workflowName = + latestExecution?.workflowName || selectedWorkflow?.name; + return { + ...INITIAL_STATE, + workflowType: workflowName || null, + workflowVersion: + latestExecution?.workflowVersion?.toString() || + selectedWorkflow?.version || + null, + workflowVersions: workflowName + ? workflowDefByVersions.get("lookups").get(workflowName) || [] + : [], + workflowInputParams: [], + workflowInputTemplate: + JSON.stringify(latestExecution?.input, null, 2) || + selectedWorkflow?.workflowInput || + "", + taskToDomain: latestExecution?.taskToDomain + ? JSON.stringify(latestExecution.taskToDomain, null, 2) + : "", + workflowCorrelationId: latestExecution?.correlationId + ? latestExecution.correlationId + : "", + lastCreatedWorkflowId: "", + }; + }, [latestExecution, selectedWorkflow, workflowDefByVersions]); + + const [runWorkflowState, setRunWorkflowState] = + useState(memorizedState); + const [errorMessage, setErrorMessage] = useState(""); + const [showCodeDialog, setShowCodeDialog] = useQueryState("displayCode", ""); + + const runWorkflowAction = useAction( + "/workflow", + "post", + { + onSuccess(data: string, input: { body: string }) { + setRunWorkflowState({ + ...runWorkflowState, + lastCreatedWorkflowId: data, + }); + setErrorMessage(""); + const existingHistory = workflowHistory || []; + const newHistoryItem = { + id: uuidv4(), + executionLink: data, + executionTime: Date.now(), + }; + const parsedBody = tryToJson(input.body); + if (parsedBody) { + Object.assign(newHistoryItem, parsedBody); + } + setWorkflowHistory([newHistoryItem, ...existingHistory].slice(0, 20)); + }, + onError: (error: Response) => { + const parseErrorResponse = async () => { + try { + const json = await error.json(); + if (json?.message) { + setErrorMessage(json.message); + } else { + setErrorMessage(GENERIC_ERROR_MESSAGE); + } + } catch { + setErrorMessage(GENERIC_ERROR_MESSAGE); + } + }; + parseErrorResponse(); + }, + }, + true, + ); + + const setLastCreatedWorkflowId = function (value: string) { + setRunWorkflowState((prevState) => ({ + ...prevState, + lastCreatedWorkflowId: value, + })); + }; + const templateForNoInput = () => { + return JSON.stringify({}, null, 2); + }; + + const setWorkflowTypeState = function (workflowType: string) { + let workflowVersionsVal = []; + + if (workflowType !== null) { + workflowVersionsVal = workflowDefByVersions + .get("lookups") + .get(workflowType); + } + + let versionObj = { + workflowVersion: null, + workflowInputParams: [] as string[], + workflowInputTemplate: "", + }; + + if (workflowVersionsVal.length > 0) { + const latestVersion = workflowVersionsVal.slice(-1).pop(); + let def: InputParameterType = { + inputParameters: [], + }; + if (latestVersion !== null) { + def = workflowDefByVersions + .get("values") + .get(workflowType) + .get(latestVersion); + } + + const templateFromInputParams = + def["inputParameters"].length > 0 + ? getTemplateFromInputParams(def["inputParameters"]) + : templateForNoInput(); + + versionObj = { + workflowVersion: latestVersion, + workflowInputParams: def["inputParameters"], + workflowInputTemplate: templateFromInputParams, + }; + } + + setRunWorkflowState({ + ...runWorkflowState, + ...versionObj, + workflowVersions: workflowVersionsVal, + workflowType: workflowType, + taskToDomain: "", + workflowCorrelationId: runWorkflowState.workflowCorrelationId, + lastCreatedWorkflowId: "", + }); + setWorkflowInputTemplatesState(versionObj.workflowInputTemplate); + }; + + const setWorkflowVersionState = function (workflowVersion: string) { + let def: InputParameterType = { + inputParameters: [], + }; + if (workflowVersion !== null) { + def = workflowDefByVersions + .get("values") + .get(runWorkflowState.workflowType) + .get(workflowVersion); + } + const templateFromInputParams = getTemplateFromInputParams( + def["inputParameters"], + ); + setRunWorkflowState({ + ...runWorkflowState, + workflowVersion: workflowVersion, + workflowInputParams: def["inputParameters"], + workflowInputTemplate: templateFromInputParams, + }); + setWorkflowInputTemplatesState(templateFromInputParams); + }; + + const runThisWorkflow = function () { + //TODO per input validation + try { + const input = + (runWorkflowState.workflowInputTemplate && + JSON.parse(runWorkflowState.workflowInputTemplate)) || + undefined; + const taskToDomain = + (runWorkflowState.taskToDomain && + JSON.parse(runWorkflowState.taskToDomain)) || + undefined; + const postObject = { + name: runWorkflowState.workflowType, + version: runWorkflowState.workflowVersion, + correlationId: runWorkflowState.workflowCorrelationId, + taskToDomain, + input, + idempotencyKey: runWorkflowState.idempotencyKey, + ...(runWorkflowState.idempotencyKey && + runWorkflowState.idempotencyStrategy && { + idempotencyStrategy: runWorkflowState.idempotencyStrategy, + }), + }; + const postBody = JSON.stringify(postObject); + // @ts-ignore + runWorkflowAction.mutate({ + body: postBody, + }); + // for localStorage + const selectedWorkflow = { + name: runWorkflowState?.workflowType, + version: runWorkflowState?.workflowVersion, + workflowInput: runWorkflowState?.workflowInputTemplate, + }; + setSelectedWorkflow(selectedWorkflow); + } catch { + setErrorMessage(INVALID_DATA_MESSAGE); + } + }; + + const clearWorkflow = function () { + setErrorMessage(""); + setRunWorkflowState(INITIAL_STATE); + setSelectedWorkflow(INITIAL_STATE); + setWorkflowInputTemplatesState(""); + }; + + const setWorkflowInputTemplatesState = function (value: string) { + setRunWorkflowState((prevState) => ({ + ...prevState, + workflowInputTemplate: value, + })); + }; + + const setWorkflowTasksToDomainState = function (value: string) { + setRunWorkflowState((prevState) => ({ + ...prevState, + taskToDomain: value, + })); + }; + + const setWorkflowCorrelationIdState = function (value: string) { + setRunWorkflowState((prevState) => ({ + ...prevState, + workflowCorrelationId: value, + })); + }; + + const handleChangeIdempotencyValues = (data: IdempotencyValuesProp) => { + setRunWorkflowState((prevState) => ({ + ...prevState, + idempotencyKey: data?.idempotencyKey, + idempotencyStrategy: data?.idempotencyStrategy, + })); + }; + + const fillRerunWorkflowFields = function (row: RunWorkflowParamType) { + // PATCH if the workflow does not exist dont populate + if (workflowNames.find((name) => name === row.name)) { + setRunWorkflowState({ + ...memorizedState, + lastCreatedWorkflowId: runWorkflowState.lastCreatedWorkflowId, + workflowCorrelationId: row.correlationId, + workflowVersion: row.version, + workflowType: row.name, + workflowVersions: workflowDefByVersions.get("lookups").get(row.name), + idempotencyKey: row?.idempotencyKey ?? "", + ...(row.idempotencyStrategy && { + idempotencyStrategy: row.idempotencyStrategy, + }), + }); + try { + if (row.taskToDomain) { + setWorkflowTasksToDomainState( + JSON.stringify(row.taskToDomain, null, 2), + ); + } + if (row.input) { + setWorkflowInputTemplatesState(JSON.stringify(row.input, null, 2)); + } + } catch (err) { + logger.error("Could not parse row:", row, err); + } + } else { + logger.warn("Workflow selected does not exist", row); + } + }; + + useEffect(() => { + if (latestExecution?.workflowName) { + setRunWorkflowState((prevState) => ({ + ...prevState, + workflowVersions: + workflowDefByVersions + .get("lookups") + .get(latestExecution.workflowName) || [], + })); + } + }, [latestExecution, workflowDefByVersions, setRunWorkflowState]); + + const navigate = useNavigate(); + + const buildQueryForCode = (): BuildQueryOutput => { + const { + workflowInputTemplate, + taskToDomain, + workflowType, + workflowVersion, + workflowCorrelationId, + idempotencyKey, + idempotencyStrategy, + } = runWorkflowState; + + const input = + (workflowInputTemplate && + tryToJson>(workflowInputTemplate)) || + {}; + + const taskToDomainVal = + (taskToDomain && tryToJson(taskToDomain)) || undefined; + + const queryObject = { + input, + taskToDomain: taskToDomainVal, + name: workflowType || "", + version: workflowVersion || "", + correlationId: workflowCorrelationId, + idempotencyKey: idempotencyKey, + ...(idempotencyKey && + idempotencyStrategy && { + idempotencyStrategy: idempotencyStrategy, + }), + }; + + return queryObject; + }; + + return ( + <> + + Run Workflow + + {showCodeDialog && ( + setShowCodeDialog("")} + buildQueryOutput={buildQueryForCode()} + /> + )} + + + + + } + options={[ + { + label: "Show as code", + onClick: () => setShowCodeDialog("active"), + }, + ]} + primaryOnClick={runThisWorkflow} + disabled={isTrialExpired} + > + Run workflow + + + } + /> + } + > + {errorMessage && ( + + { + setLastCreatedWorkflowId(""); + setErrorMessage(""); + }} + severity="error" + > + {errorMessage} + + + )} + {runWorkflowState.lastCreatedWorkflowId !== "" && ( + + { + setLastCreatedWorkflowId(""); + }} + severity="success" + > + Workflow created :  + + {runWorkflowState.lastCreatedWorkflowId} + + + + )} + + + + + + + { + setWorkflowTypeState(val); + }} + value={runWorkflowState.workflowType} + autoFocus + required + /> + + + setWorkflowVersionState(val)} + value={runWorkflowState.workflowVersion} + /> + + + + setWorkflowInputTemplatesState(value) + } + options={{ + scrollBeyondLastLine: false, + wrappingStrategy: "advanced", + lightbulb: { + enabled: editor.ShowLightbulbIconMode.On, + }, + quickSuggestions: true, + lineNumbers: "on", + wordWrap: "on", + glyphMargin: false, + folding: false, + lineDecorationsWidth: 10, + lineNumbersMinChars: 0, + renderLineHighlight: "none", + hideCursorInOverviewRuler: false, + overviewRulerBorder: false, + automaticLayout: true, // Important + }} + autoformat + /> + + + + + setWorkflowCorrelationIdState(val) + } + /> + + + + setWorkflowTasksToDomainState(value) + } + /> + + + + + + + + + + + + + ); +} diff --git a/ui-next/src/pages/runWorkflow/RunWorkflowApiSearchModal.tsx b/ui-next/src/pages/runWorkflow/RunWorkflowApiSearchModal.tsx new file mode 100644 index 0000000000..101dd576c3 --- /dev/null +++ b/ui-next/src/pages/runWorkflow/RunWorkflowApiSearchModal.tsx @@ -0,0 +1,131 @@ +import { ApiSearchModal } from "components/v1/ApiSearchModal/ApiSearchModal"; +import { curlHeaders } from "shared/CodeModal/curlHeader"; +import { toCodeT, useParamsToSdk } from "shared/CodeModal/hook"; +import { SupportedDisplayTypes } from "shared/CodeModal/types"; +import { IdempotencyStrategyEnum } from "./types"; + +export type BuildQueryOutput = { + input?: Record; + taskToDomain?: object; + name: string; + version: string | null; + correlationId: string; + idempotencyKey?: string; + idempotencyStrategy?: IdempotencyStrategyEnum; +}; + +interface RunWorkflowApiSearchModalProps { + buildQueryOutput: BuildQueryOutput; + onClose: () => void; +} + +const buildCurlCode = ( + buildQueryOutput: BuildQueryOutput, + accessToken: string, +) => { + const { + correlationId, + name, + version, + input, + taskToDomain, + idempotencyKey, + idempotencyStrategy, + } = buildQueryOutput; + + const headers = { + ...curlHeaders(accessToken), + "Content-Type": "application/json", + }; + + const dataRawJSON = { + name: name, + version: version, + input, + correlationId: correlationId, + idempotencyKey: idempotencyKey, + ...(idempotencyStrategy && { idempotencyStrategy: idempotencyStrategy }), + ...(taskToDomain && { taskToDomain: taskToDomain }), + }; + + const curlCommand = `curl '${ + window.location.origin + }/api/workflow' \\${Object.entries(headers) + .map(([key, value]) => `\n-H '${key}: ${value}' \\`) + .join("")}\n--data-raw '${JSON.stringify(dataRawJSON)}'`; + + return curlCommand; +}; + +const buildJsCode = ( + buildQueryOutput: BuildQueryOutput, + accessToken: string, +) => { + const { + correlationId, + name, + version, + input, + taskToDomain, + idempotencyKey, + idempotencyStrategy, + } = buildQueryOutput; + + return `import { orkesConductorClient, WorkflowExecutor } from "@io-orkes/conductor-javascript"; + +async function runWorkflow() { + const client = await orkesConductorClient({ + TOKEN: "${accessToken}", + serverUrl: "${window.location.origin}/api" + }); + const executor = new WorkflowExecutor(client); + + const data = ${`{ + name: "${name}", + version: "${version}", + input: ${JSON.stringify(input)}, + correlationId: "${correlationId}", + idempotencyKey:"${idempotencyKey}", + ${ + idempotencyStrategy ? `idempotencyStrategy:"${idempotencyStrategy}",` : "" + } + ${taskToDomain ? `taskToDomain: ${JSON.stringify(taskToDomain)},` : ""} + };`.replace(/^\s*[\r\n]/gm, "")} + + const result = await executor.startWorkflow(data); + + return result; +} + +runWorkflow(); + `; +}; + +const toCodeMap: toCodeT = { + curl: buildCurlCode, + javascript: buildJsCode, +}; + +const RunWorkflowApiSearchModal = ({ + onClose, + buildQueryOutput, +}: RunWorkflowApiSearchModalProps) => { + const { selectedLanguage, setSelectedLanguage, code } = + useParamsToSdk(buildQueryOutput, toCodeMap); + + return ( + { + setSelectedLanguage(val); + }} + dialogTitle="Run Workflow API" + dialogHeaderText="Here is the code for the run workflow." + languages={Object.keys(toCodeMap) as SupportedDisplayTypes[]} + /> + ); +}; + +export { RunWorkflowApiSearchModal }; diff --git a/ui-next/src/pages/runWorkflow/index.ts b/ui-next/src/pages/runWorkflow/index.ts new file mode 100644 index 0000000000..fc48430ffc --- /dev/null +++ b/ui-next/src/pages/runWorkflow/index.ts @@ -0,0 +1 @@ +export * from "./RunWorkflow"; diff --git a/ui-next/src/pages/runWorkflow/runWorkflowUtils.ts b/ui-next/src/pages/runWorkflow/runWorkflowUtils.ts new file mode 100644 index 0000000000..6bbe9e54b7 --- /dev/null +++ b/ui-next/src/pages/runWorkflow/runWorkflowUtils.ts @@ -0,0 +1,151 @@ +import { logger } from "utils/logger"; +import { tryToJson } from "utils/utils"; +import _isArray from "lodash/isArray"; +import { + DeletedWfNameType, + DeletedWfVersionType, + ParsedSelectedWorkflowType, +} from "./types"; + +export const removeCopyFromStorage = (context: any): Promise => { + removeCachedChangesFromWorkflow( + context.workflowName, + context.currentVersion, + context.isNewWorkflow, + context.currentWf?.version, + ); + + return Promise.resolve(true); +}; + +const isNotAValidVersion = (deletedwfversion: DeletedWfVersionType) => { + return deletedwfversion === null || isNaN(deletedwfversion as number); +}; + +const cleanupHistoryInLocalStorage = ( + deletedwfname: DeletedWfNameType, + deletedwfversion: DeletedWfVersionType, +) => { + const history = localStorage.getItem("workflowHistory"); + const parsedHistory = tryToJson(history); + if (_isArray(parsedHistory)) { + const filteredhistory = parsedHistory.filter( + (item: { name: string; version: string }) => { + const isDeletedWorkflow = + item.name === deletedwfname && + ((isNotAValidVersion(deletedwfversion) && item.version === null) || + parseInt(item.version) === deletedwfversion); + return !isDeletedWorkflow; + }, + ); + try { + localStorage.setItem("workflowHistory", JSON.stringify(filteredhistory)); + } catch (error) { + logger.error("Error stringifying filteredhistory:", error); + } + } +}; + +const removeSelectedWfInLocalStorage = (deletedWfName: DeletedWfNameType) => { + const selectedWorkflow = localStorage.getItem("selectedWorkflow"); + const parsedSelectedWorkflow = + tryToJson(selectedWorkflow); + if ( + !!parsedSelectedWorkflow && + parsedSelectedWorkflow.name === deletedWfName + ) { + localStorage.removeItem("selectedWorkflow"); + } +}; + +export const extractKeyFromContext = ({ + workflowName, + currentVersion, + isNewWorkflow = false, +}: { + workflowName: string; + currentVersion?: number; + isNewWorkflow?: boolean; +}) => { + return isNewWorkflow + ? "newWorkflowDef" + : `${workflowName}/${currentVersion ?? ""}`; +}; + +const localcopytimekey = "_localcopyupdatedtime"; + +export const addLocalCopyTime = (wfKey: any) => { + localStorage.setItem( + wfKey + localcopytimekey, + new Date().toLocaleString("en-US"), + ); +}; +export const removeLocalCopyTime = (wfKey: any) => { + localStorage.removeItem(wfKey + localcopytimekey); +}; +export const getLocalCopyTime = (wfKey: any) => { + return localStorage.getItem(wfKey + localcopytimekey); +}; + +export const removeCachedChangesFromWorkflow = ( + deletedWfName: DeletedWfNameType, + deletedWfVersion?: DeletedWfVersionType, + isNewWorkflow = false, + previousVersion?: DeletedWfVersionType, +) => { + if (deletedWfName != null) { + const context = { + workflowName: deletedWfName, + currentVersion: deletedWfVersion, + isNewWorkflow, + }; + const wfKey = extractKeyFromContext(context); + localStorage.removeItem(wfKey); + + const wfKeyLastVersion = extractKeyFromContext({ + ...context, + currentVersion: undefined, + }); + localStorage.removeItem(wfKeyLastVersion); + + if (previousVersion) { + const wfKeyPreviousVersion = extractKeyFromContext({ + ...context, + currentVersion: previousVersion, + }); + localStorage.removeItem(wfKeyPreviousVersion); + removeLocalCopyTime(wfKeyPreviousVersion); + } + + removeLocalCopyTime(wfKeyLastVersion); + removeLocalCopyTime(wfKey); + logger.log("Removing version from storage", wfKey); + } +}; + +export const removeDeletedWorkflow = ( + deletedWfName: DeletedWfNameType, + deletedWfVersion: DeletedWfVersionType, + isNewWorkflow = false, +) => { + cleanupHistoryInLocalStorage(deletedWfName, deletedWfVersion); + removeSelectedWfInLocalStorage(deletedWfName); + removeCopyFromStorage({ + workflowName: deletedWfName, + currentVersion: deletedWfVersion, + isNewWorkflow, + }); +}; + +export function getTemplateFromInputParams(inputParamsArray: any) { + if (!inputParamsArray) { + return ""; + } + const input: { [key: string]: string } = {}; + if (Array.isArray(inputParamsArray)) { + inputParamsArray.forEach((val: any) => { + input[val] = ""; + }); + } + return JSON.stringify(input, null, 2); +} diff --git a/ui-next/src/pages/runWorkflow/types.ts b/ui-next/src/pages/runWorkflow/types.ts new file mode 100644 index 0000000000..b43177be11 --- /dev/null +++ b/ui-next/src/pages/runWorkflow/types.ts @@ -0,0 +1,16 @@ +export type SelectedWorkflowType = { + name: string; + version: number | string; +}; + +export type DeletedWfNameType = string | undefined; + +export type DeletedWfVersionType = number | undefined; + +export type ParsedSelectedWorkflowType = SelectedWorkflowType | undefined; + +export enum IdempotencyStrategyEnum { + FAIL = "FAIL", + RETURN_EXISTING = "RETURN_EXISTING", + FAIL_ON_RUNNING = "FAIL_ON_RUNNING", +} diff --git a/ui-next/src/pages/scheduler/CronExpressionHelp.tsx b/ui-next/src/pages/scheduler/CronExpressionHelp.tsx new file mode 100644 index 0000000000..d772dacbbf --- /dev/null +++ b/ui-next/src/pages/scheduler/CronExpressionHelp.tsx @@ -0,0 +1,121 @@ +import { Box } from "@mui/material"; +import { CRON_COLORS_BY_POSITION } from "./constants"; + +type Props = { + highlightedPart?: number | null; +}; + +const CronExpressionHelp = ({ highlightedPart }: Props) => { + const items = [ + { text: "Second (0-59)", color: CRON_COLORS_BY_POSITION[0] }, + { text: "Minute (0-59)", color: CRON_COLORS_BY_POSITION[1] }, + { text: "Hour (0-23)", color: CRON_COLORS_BY_POSITION[2] }, + { text: "Day of Month (1-31)", color: CRON_COLORS_BY_POSITION[3] }, + { text: "Month (1-12, JAN-DEC)", color: CRON_COLORS_BY_POSITION[4] }, + { text: "Day of Week (1-7 or MON-SUN)", color: CRON_COLORS_BY_POSITION[5] }, + ]; + const width = 180; + const height = 120; + + return ( + + + + {items.map((item, index) => { + const xStep = width / items.length; + const yStep = height / items.length; + return ( + + ); + })} + + + {items.map((item, index) => { + const blockWidth = width / items.length; + return ( + + * + + ); + })} + + + + + {items.map((item, index) => { + const blockHeight = height / items.length; + return ( + + {item.text} + + ); + })} + + + + ); +}; + +export default CronExpressionHelp; diff --git a/ui-next/src/pages/scheduler/SaveProtectionPrompt.tsx b/ui-next/src/pages/scheduler/SaveProtectionPrompt.tsx new file mode 100644 index 0000000000..4e1cc3b8f3 --- /dev/null +++ b/ui-next/src/pages/scheduler/SaveProtectionPrompt.tsx @@ -0,0 +1,162 @@ +import fastDeepEqual from "fast-deep-equal"; +import _isEmpty from "lodash/isEmpty"; +import { FunctionComponent, useMemo } from "react"; +import BlockNavigationWithConfirmation from "shared/BlockNavigationWithConfirmation"; +import { useSaveProtection } from "shared/useSaveProtection"; +import { ActorRef, AnyEventObject, EventObject } from "xstate"; + +export interface SaveProtectionPromptProps { + isInFormView: number; + data: Record; + initialFormData: Record; + changedCodeData: Record; + actor?: ActorRef; + isSaveInProgress?: boolean; + hasErrors?: boolean; + onSave?: () => void; +} + +// Component that uses useSaveProtection with an actor +const SaveProtectionPromptWithActor: FunctionComponent<{ + actor: ActorRef; + noFormChanges: boolean; + isSaveInProgressProp?: boolean; + hasErrorsProp?: boolean; + onSave?: () => void; +}> = ({ + actor, + noFormChanges, + isSaveInProgressProp, + hasErrorsProp, + onSave, +}) => { + const saveProtectionResult = useSaveProtection< + Record, + EventObject + >({ + actor, + noFormChanges, + isSaveInProgress: (state) => { + // Check if we're in a saving state + const context = state.context as Record; + if (typeof context.isSaving === "boolean") { + return context.isSaving; + } + if (typeof context.isConfirmingSave === "boolean") { + return context.isConfirmingSave; + } + return isSaveInProgressProp ?? false; + }, + hasErrors: (state) => { + // Check for errors in context + const context = state.context as Record; + if (typeof context.hasErrors === "boolean") { + return context.hasErrors; + } + if (typeof context.couldNotParseJson === "boolean") { + return context.couldNotParseJson; + } + if (context.error !== undefined) { + return true; + } + return hasErrorsProp ?? false; + }, + handleSaveAction: onSave + ? () => { + onSave(); + } + : () => { + // Default no-op if no handler provided + }, + }); + + return ( + + Your recent changes are not saved to the server. To run the new + schedule, you have to save your progress. + + } + title={"Unsaved schedule confirmation"} + block={saveProtectionResult.showPrompt} + onSave={saveProtectionResult.handleSave} + successfulSave={saveProtectionResult.successfulSave} + hasErrors={saveProtectionResult.hasErrors} + /> + ); +}; + +// Component that uses fallback logic without actor +const SaveProtectionPromptWithoutActor: FunctionComponent<{ + noFormChanges: boolean; + isSaveInProgress?: boolean; + hasErrors?: boolean; + onSave?: () => void; +}> = ({ noFormChanges, isSaveInProgress, hasErrors, onSave }) => { + const showPrompt = useMemo( + () => !noFormChanges && !(isSaveInProgress ?? false), + [noFormChanges, isSaveInProgress], + ); + + return ( + + Your recent changes are not saved to the server. To run the new + schedule, you have to save your progress. + + } + title={"Unsaved schedule confirmation"} + block={showPrompt} + onSave={onSave ?? (() => {})} + successfulSave={undefined} + hasErrors={hasErrors ?? false} + /> + ); +}; + +export const SaveProtectionPrompt: FunctionComponent< + SaveProtectionPromptProps +> = ({ + data, + initialFormData, + changedCodeData, + isInFormView, + actor, + isSaveInProgress: isSaveInProgressProp, + hasErrors: hasErrorsProp, + onSave, +}) => { + const noFormChanges = useMemo(() => { + const formResult = fastDeepEqual(data, initialFormData); + const codeResult = !_isEmpty(changedCodeData) + ? fastDeepEqual(data, changedCodeData) + : true; + return isInFormView ? formResult : codeResult; + }, [data, initialFormData, changedCodeData, isInFormView]); + + // Use actor-based component if actor is provided, otherwise use fallback + if (actor) { + return ( + + ); + } + + return ( + + ); +}; diff --git a/ui-next/src/pages/scheduler/Schedule.tsx b/ui-next/src/pages/scheduler/Schedule.tsx new file mode 100644 index 0000000000..ce00a2d308 --- /dev/null +++ b/ui-next/src/pages/scheduler/Schedule.tsx @@ -0,0 +1,780 @@ +import { Monaco } from "@monaco-editor/react"; +import { + Box, + CircularProgress, + Grid, + Paper, + SxProps, + Tab, + Tabs, + Theme, + useMediaQuery, +} from "@mui/material"; +import { LinearProgress } from "components"; +import { DocLink } from "components/DocLink"; +import { SnackbarMessage } from "components/SnackbarMessage"; +import ConductorInput from "components/v1/ConductorInput"; +import { MessageContext } from "components/v1/layout/MessageContext"; +import { ConductorSectionHeader } from "components/v1/layout/section/ConductorSectionHeader"; +import { IdempotencyStrategyEnum } from "pages/runWorkflow/types"; +import { useCallback, useContext, useMemo, useState } from "react"; +import { Helmet } from "react-helmet"; +import { useLocation, useParams } from "react-router"; +import SectionContainer from "shared/SectionContainer"; +import { colors } from "theme/tokens/variables"; +import { IObject } from "types/common"; +import { DOC_LINK_URL } from "utils/constants/docLink"; +import { SCHEDULER_DEFINITION_URL } from "utils/constants/route"; +import { usePushHistory } from "utils/hooks/usePushHistory"; +import { getErrors } from "utils/index"; +import { useWorkflowDefsByVersions } from "utils/query"; +import { CronExpressionSection } from "./components/CronExpressionSection"; +import { ScheduleTimingSection } from "./components/ScheduleTimingSection"; +import { WorkflowConfigSection } from "./components/WorkflowConfigSection"; +import { useCronExpression } from "./hooks/useCronExpression"; +import { useScheduleFormHandlers } from "./hooks/useScheduleFormHandlers"; +import { useScheduleState } from "./hooks/useScheduleState"; +import { useWorkflowConfig } from "./hooks/useWorkflowConfig"; +import { SaveProtectionPrompt } from "./SaveProtectionPrompt"; +import ScheduleButtons from "./ScheduleButtons"; +import ScheduleDiffEditor from "./ScheduleDiffEditor"; +import { useSaveSchedule, useSchedule } from "./schedulerHooks"; +import { + codeToFormData, + formToCodeData, + getDateFromField, + JSONParse, +} from "./utils/scheduleTransformers"; + +export type ScheduleType = { + name: string; + description?: string; + cronExpression: string; + paused: boolean; + runCatchupScheduleInstances: boolean; + workflowType: string | null; + workflowVersion: string | null; + workflowVersions: string[]; + workflowInputTemplate: string; + taskToDomain: string; + workflowCorrelationId: string; + workflowIdempotencyKey?: string; + workflowIdempotencyStrategy?: IdempotencyStrategyEnum; + workflowDef: string | null; + externalInputPayloadStoragePath?: string; + scheduleStartTime: string | number; + scheduleEndTime: string | number; + priority: string; + zoneId?: string; + startWorkflowRequest?: Record; +}; + +export function Schedule() { + const { setMessage } = useContext(MessageContext); + const location = useLocation(); + const latestExecution = useMemo(() => location.state?.execution, [location]); + const [selectedTemplate, setSelectedTemplate] = useState(""); + const [timeoutHandler, setTimeoutHandler] = useState | null>(null); + + const params = useParams(); + const navigate = usePushHistory(); + const isNewScheduleDef = location.pathname === SCHEDULER_DEFINITION_URL.NEW; + let scheduleNameFromUrl = "New Scheduler"; + const isMDWidth = useMediaQuery((theme: Theme) => theme.breakpoints.up("md")); + + if (!isNewScheduleDef) { + scheduleNameFromUrl = params.name || "New Scheduler"; + } + + const { data: schedule, isLoading } = useSchedule( + isNewScheduleDef ? null : scheduleNameFromUrl, + ); + + const workflowDefByVersions = useWorkflowDefsByVersions(); + + // Custom hooks for state management + const { + scheduleState, + setScheduleState, + original, + initializeFromSchedule, + initializeFromExecution, + } = useScheduleState(latestExecution, schedule); + + const { + workflowNames, + workflowVersions, + setWorkflowType, + setWorkflowVersion, + } = useWorkflowConfig( + workflowDefByVersions, + scheduleState.workflowType || null, + scheduleState.workflowVersions, + scheduleState.workflowInputTemplate, + ); + + const [errorMessage, setErrorMessage] = useState(null); + const [errors, setErrors] = useState(null); + const [couldNotParseJson, setCouldNotParseJson] = useState(false); + + const clearError = useCallback( + (field: string) => { + if (errors) { + const updatedErrors = { ...errors }; + delete updatedErrors[field]; + setErrors(updatedErrors); + } + }, + [errors, setErrors], + ); + + const formHandlers = useScheduleFormHandlers( + scheduleState, + setScheduleState, + setErrors, + clearError, + errors, + setCouldNotParseJson, + () => {}, // setHighlightedPart will be handled by cron hook + ); + + const cronHook = useCronExpression( + scheduleState.cronExpression, + scheduleState.zoneId || "UTC", + (error) => { + if (error) { + setErrors((prevErrors: IObject | null) => ({ + ...prevErrors, + cronExpression: error, + })); + } else { + clearError("cronExpression"); + } + }, + ); + + const { mutate: saveSchedule, isLoading: isSavingSchedule } = useSaveSchedule( + { + onSuccess: () => { + setMessage({ + text: "Schedule definition saved successfully.", + severity: "success", + }); + navigate(SCHEDULER_DEFINITION_URL.BASE); + }, + + onError: async (response: Response) => { + const errors = await getErrors(response, (res) => ({ + message: `Error - ${res.status} - ${res.statusText}`, + })); + console.error("Errors: ", errors); + setErrors(errors); + if (response.status === 403) { + setErrorMessage( + `Error - You don't have permissions to schedule the selected workflow.`, + ); + } else { + if (errors.message) { + setErrorMessage(`Error - ${response.status} - ${errors.message}`); + } else { + setErrorMessage( + `Error - ${response.status} - ${response.statusText}`, + ); + } + } + setTimeoutHandler(setTimeout(() => setErrorMessage(null), 5000)); + cancelConfirmSave(); + }, + }, + ); + + // Memoized handlers using custom hooks + const handleWorkflowTypeChange = useCallback( + (workflowType: string) => { + setCouldNotParseJson(false); + if (errors && errors["startWorkflowRequest.name"]) { + clearError("startWorkflowRequest.name"); + } + const result = setWorkflowType(workflowType); + setScheduleState((prevState) => ({ + ...prevState, + workflowVersions: result.workflowVersions, + workflowVersion: "", + workflowType: workflowType, + workflowCorrelationId: "", + workflowInputTemplate: result.workflowInputTemplate, + })); + }, + [setWorkflowType, setScheduleState, errors, clearError], + ); + + const handleWorkflowVersionChange = useCallback( + (workflowVersion: string | null) => { + const result = setWorkflowVersion( + workflowVersion, + scheduleState.workflowType || null, + ); + setScheduleState((prevState) => ({ + ...prevState, + workflowVersion: + workflowVersion === "Latest version" ? "" : workflowVersion, + workflowInputTemplate: result.workflowInputTemplate, + })); + }, + [setWorkflowVersion, setScheduleState, scheduleState.workflowType], + ); + + // Initialize state when schedule data changes + useMemo(() => { + if (schedule) { + initializeFromSchedule(schedule); + } + }, [schedule, initializeFromSchedule]); + + useMemo(() => { + if (latestExecution?.workflowName) { + initializeFromExecution(latestExecution); + } + }, [latestExecution, initializeFromExecution]); + + // Memoized cron expression handler + const handleCronExpressionChange = useCallback( + (value: string, timezone: string) => { + cronHook.setCronExpression(value, timezone); + setScheduleState((prevState) => ({ + ...prevState, + cronExpression: value, + })); + }, + [cronHook, setScheduleState], + ); + + // Memoized values + const minWidthCronExpression = useMemo(() => { + if (selectedTemplate && isMDWidth) { + return "470px"; + } else if (!selectedTemplate && isMDWidth) { + return "initial"; + } + return "100%"; + }, [selectedTemplate, isMDWidth]); + + // Memoized handlers + const handleZoneIdChange = useCallback( + (value: string) => { + formHandlers.setZoneId(value); + handleCronExpressionChange(scheduleState.cronExpression, value); + }, + [formHandlers, handleCronExpressionChange, scheduleState.cronExpression], + ); + + const clearErrors = useCallback(() => { + if (timeoutHandler) { + clearTimeout(timeoutHandler); + } + setErrorMessage(""); + setErrors(null); + }, [timeoutHandler]); + + const saveScheduleSubmit = useCallback(() => { + clearErrors(); + + const start = getDateFromField(scheduleState.scheduleStartTime); + const to = getDateFromField(scheduleState.scheduleEndTime); + + let input; + try { + input = JSONParse(scheduleState.workflowInputTemplate); + } catch { + setErrorMessage("Invalid JSON: input params"); + return; + } + + let taskToDomain; + try { + taskToDomain = JSONParse(scheduleState.taskToDomain); + } catch { + setErrorMessage("Invalid JSON: tasks to domain mapping"); + return; + } + + const body = JSON.stringify({ + id: schedule?.id, + paused: scheduleState.paused, + runCatchupScheduleInstances: scheduleState.runCatchupScheduleInstances, + name: scheduleState.name, + description: scheduleState.description, + cronExpression: scheduleState.cronExpression, + scheduleStartTime: start, + scheduleEndTime: to, + startWorkflowRequest: { + name: scheduleState.workflowType, + version: scheduleState.workflowVersion, + input, + correlationId: scheduleState.workflowCorrelationId, + idempotencyKey: scheduleState?.workflowIdempotencyKey, + idempotencyStrategy: scheduleState?.workflowIdempotencyStrategy, + taskToDomain, + workflowDef: scheduleState.workflowDef, + externalInputPayloadStoragePath: + scheduleState.externalInputPayloadStoragePath, + priority: scheduleState.priority, + }, + zoneId: scheduleState.zoneId, + }); + + saveSchedule({ body } as any); + }, [scheduleState, schedule, clearErrors, setErrorMessage, saveSchedule]); + + const clearScheduleForm = useCallback(() => { + if (schedule) { + initializeFromSchedule(schedule); + } else { + // Reset to initial state + setScheduleState({ + name: "", + description: "", + cronExpression: "", + paused: false, + runCatchupScheduleInstances: false, + workflowType: null, + workflowVersion: null, + workflowVersions: [], + workflowInputTemplate: "", + taskToDomain: "", + workflowCorrelationId: "", + workflowIdempotencyKey: undefined, + workflowIdempotencyStrategy: undefined, + workflowDef: null, + externalInputPayloadStoragePath: undefined, + scheduleStartTime: "", + scheduleEndTime: "", + priority: "", + zoneId: "UTC", + }); + } + setIsInFormView(1); + }, [schedule, initializeFromSchedule, setScheduleState]); + + const [isInFormView, setIsInFormView] = useState(1); + const [isConfirmingSave, setIsConfirmingSave] = useState(false); + const [newData, setNewData] = useState(""); + const [transitionData, setTransitionData] = + useState | null>(null); + const [interimString, setInterimString] = useState(""); + + const MAX_WIDTH = "920px"; + const containerStyle: SxProps = { + maxWidth: MAX_WIDTH, + color: (theme) => + theme.palette?.mode === "dark" ? colors.gray14 : undefined, + backgroundColor: (theme) => theme.palette.customBackground.form, + px: 4, + }; + + const setSaveConfirmationOpen = useCallback(() => { + setIsConfirmingSave(true); + setIsInFormView(0); + if (interimString !== "") { + const body = codeToFormData(interimString, scheduleState); + setScheduleState(body); + setInterimString(""); + setTransitionData(null); + setNewData(interimString); + } else { + const start = getDateFromField(scheduleState.scheduleStartTime); + const to = getDateFromField(scheduleState.scheduleEndTime); + + let input; + try { + input = JSONParse(scheduleState.workflowInputTemplate); + } catch { + setErrorMessage("Invalid JSON: Input Params"); + return; + } + + let taskToDomain; + try { + taskToDomain = JSONParse(scheduleState.taskToDomain); + } catch { + setErrorMessage("Invalid JSON: Tasks to Domain Mapping"); + return; + } + + const body = JSON.stringify( + { + id: schedule?.id, + paused: scheduleState.paused, + runCatchupScheduleInstances: + scheduleState.runCatchupScheduleInstances, + name: scheduleState.name, + description: scheduleState.description, + cronExpression: scheduleState.cronExpression, + scheduleStartTime: start, + scheduleEndTime: to, + startWorkflowRequest: { + name: scheduleState.workflowType, + version: scheduleState.workflowVersion, + input: input ? input : {}, + correlationId: scheduleState.workflowCorrelationId, + idempotencyKey: scheduleState?.workflowIdempotencyKey, + idempotencyStrategy: scheduleState?.workflowIdempotencyStrategy, + taskToDomain: taskToDomain ? taskToDomain : {}, + externalInputPayloadStoragePath: + scheduleState.externalInputPayloadStoragePath, + priority: scheduleState.priority, + }, + zoneId: scheduleState.zoneId, + }, + null, + 2, + ); + setNewData(body); + } + }, [ + interimString, + scheduleState, + schedule, + setErrorMessage, + setScheduleState, + ]); + + const cancelConfirmSave = useCallback(() => { + const body = JSON.parse(newData); + setTransitionData(body); + setIsConfirmingSave(false); + }, [newData]); + + const handleChangeTab = useCallback( + (value: number) => { + if (value === 0) { + const body = formToCodeData(scheduleState, schedule); + setTransitionData(body); + } else { + if (interimString !== "") { + const body = codeToFormData(interimString, scheduleState); + body.workflowVersions = scheduleState.workflowVersions; + setScheduleState(body); + setInterimString(""); + setTransitionData(null); + } else { + if (newData) { + const body = codeToFormData(newData, scheduleState); + body.workflowVersions = scheduleState.workflowVersions; + setScheduleState(body); + setInterimString(""); + setTransitionData(null); + setNewData(""); + } else { + const body = codeToFormData( + JSON.stringify(transitionData), + scheduleState, + ); + setScheduleState(body); + } + } + } + setIsInFormView(value); + }, + [ + scheduleState, + schedule, + interimString, + newData, + transitionData, + setScheduleState, + ], + ); + + const handleChangeTransitionData = useCallback( + (data: string) => { + let parsedData: ScheduleType; + try { + parsedData = JSON.parse(data); + setCouldNotParseJson(false); + } catch { + setCouldNotParseJson(true); + return; + } + setScheduleState((prevState) => ({ + ...prevState, + name: parsedData?.name, + })); + setInterimString(data); + }, + [setScheduleState], + ); + + const diffEditorDidMount = useCallback( + (editor: Monaco) => { + const modifiedEditor = editor.getModifiedEditor(); + modifiedEditor.onDidChangeModelContent(() => { + const maybeText = modifiedEditor.getValue(); + if (typeof maybeText === "string") { + try { + JSON.parse(maybeText); + } catch { + return; + } + const body = codeToFormData(maybeText, scheduleState); + setNewData(maybeText); + setScheduleState(body); + } + }); + }, + [scheduleState, setScheduleState], + ); + + const initialFormData = useMemo( + () => + isNewScheduleDef + ? { + ...codeToFormData(JSON.stringify(original), scheduleState), + taskToDomain: "", + workflowInputTemplate: "", + workflowDef: null, + } + : codeToFormData(JSON.stringify(original), scheduleState), + [isNewScheduleDef, original, scheduleState], + ); + + const changedCodeData = useMemo( + () => (interimString ? codeToFormData(interimString, scheduleState) : {}), + [interimString, scheduleState], + ); + + return ( + + + Schedule Editor - {scheduleNameFromUrl} + + + } + /> + } + > + + {(isLoading || isSavingSchedule) && ( + + )} + + {errorMessage && ( + setErrorMessage(null)} + /> + )} + + + + handleChangeTab(newValue)} + > + + + + + + + + + {!isLoading ? ( + + theme.palette?.mode === "dark" + ? colors.gray14 + : undefined, + backgroundColor: (theme) => + theme.palette.customBackground.form, + }} + > + {isInFormView ? ( + + + + + formHandlers.setScheduleNewState("name", val) + } + error={errors?.name !== undefined} + helperText={errors ? errors?.name : undefined} + tooltip={{ + title: "Name", + content: "Changing name saves as a new schedule.", + }} + /> + + + + formHandlers.setScheduleNewState( + "description", + value, + ) + } + placeholder="Enter description" + /> + + + + + + + ) : ( + + + + )} + + ) : ( + + + + )} + + + + + + ); +} diff --git a/ui-next/src/pages/scheduler/ScheduleButtons.tsx b/ui-next/src/pages/scheduler/ScheduleButtons.tsx new file mode 100644 index 0000000000..f4bf701be7 --- /dev/null +++ b/ui-next/src/pages/scheduler/ScheduleButtons.tsx @@ -0,0 +1,78 @@ +import { FunctionComponent } from "react"; +import { Button, Stack, useMediaQuery } from "@mui/material"; +import SaveIcon from "components/v1/icons/SaveIcon"; +import XCloseIcon from "components/v1/icons/XCloseIcon"; +import ResetIcon from "components/v1/icons/ResetIcon"; +import { Theme } from "@mui/material/styles"; +import { useAuth } from "shared/auth"; + +export interface ScheduleButtonsProps { + isConfirmingSave: boolean; + couldNotParseJson: boolean; + cancelConfirmSave: () => void; + saveScheduleSubmit: () => void; + clearScheduleForm: () => void; + setSaveConfirmationOpen: () => void; +} + +const VALID_WIDTH_BREAKPOINT = 491; + +const ScheduleButtons: FunctionComponent = ({ + isConfirmingSave, + couldNotParseJson, + cancelConfirmSave, + saveScheduleSubmit, + clearScheduleForm, + setSaveConfirmationOpen, +}) => { + const { isTrialExpired } = useAuth(); + const isValidWidth = useMediaQuery((theme: Theme) => + theme.breakpoints.down(VALID_WIDTH_BREAKPOINT), + ); + + return ( + + {isConfirmingSave ? ( + + + + + ) : ( + + + + + )} + + ); +}; +export default ScheduleButtons; diff --git a/ui-next/src/pages/scheduler/ScheduleDiffEditor.jsx b/ui-next/src/pages/scheduler/ScheduleDiffEditor.jsx new file mode 100644 index 0000000000..680ee1bd1a --- /dev/null +++ b/ui-next/src/pages/scheduler/ScheduleDiffEditor.jsx @@ -0,0 +1,95 @@ +import Editor from "@monaco-editor/react"; +import { Box } from "@mui/material"; +import { DiffEditor } from "components/DiffEditor/DiffEditor"; +import { useCallback, useContext, useRef } from "react"; +import { defaultEditorOptions } from "shared/editor"; +import { ColorModeContext } from "theme/material/ColorModeContext"; +import { configureMonaco } from "utils/monacoUtils/CodeEditorUtils"; + +function ScheduleDiffEditor({ + data, + newData, + original, + handleChange, + isConfirmingSave, + handleDiffEditorMount, +}) { + const { mode } = useContext(ColorModeContext); + const monacoObjects = useRef(null); + const minEditor_Width = 590; + const darkMode = mode === "dark"; + const editorTheme = darkMode ? "vs-dark" : "vs-light"; + const editorState = { + editorOptions: { + ...defaultEditorOptions, + selectOnLineNumbers: true, + }, + }; + + const handleChangeTest = (changedData) => { + handleChange(changedData); + }; + const editorDidMount = useCallback( + (editor) => { + monacoObjects.current = editor; + }, + [monacoObjects], + ); + const handleEditorWillMount = useCallback((monaco) => { + configureMonaco(monaco); + }, []); + + return ( + <> + + + {isConfirmingSave ? ( + + ) : ( + { + handleChangeTest(maybeText); + }} + /> + )} + + + + ); +} +export default ScheduleDiffEditor; diff --git a/ui-next/src/pages/scheduler/TimezonePicker.tsx b/ui-next/src/pages/scheduler/TimezonePicker.tsx new file mode 100644 index 0000000000..30c364a05c --- /dev/null +++ b/ui-next/src/pages/scheduler/TimezonePicker.tsx @@ -0,0 +1,31 @@ +import { ConductorAutoComplete } from "components/v1/ConductorAutoComplete"; +import timezones from "./timezones.json"; + +type TimezonePickerProps = { + timezone: string; + onChange: (newValue: any) => void; + error: boolean; + helperText: string; +}; + +export const TimezonePicker = ({ + timezone, + onChange, + error, + helperText, +}: TimezonePickerProps) => { + return ( + onChange(value)} + /> + ); +}; diff --git a/ui-next/src/pages/scheduler/__tests__/Schedule.test.tsx b/ui-next/src/pages/scheduler/__tests__/Schedule.test.tsx new file mode 100644 index 0000000000..7888ee54aa --- /dev/null +++ b/ui-next/src/pages/scheduler/__tests__/Schedule.test.tsx @@ -0,0 +1,682 @@ +import { ThemeProvider, createTheme } from "@mui/material/styles"; +import { + fireEvent, + render, + screen, + renderHook, + waitFor, +} from "@testing-library/react"; +import React from "react"; +import { formatInTimeZone } from "utils/date"; +import { TimezonePicker } from "../TimezonePicker"; +import { cronExpressionIsValid } from "utils/cronHelpers"; +import cronstrue from "cronstrue"; +import { useCronExpression } from "../hooks/useCronExpression"; + +// Mock the timezones data +vi.mock("../timezones.json", () => ({ + default: [ + "UTC", + "America/New_York", + "Europe/London", + "Asia/Tokyo", + "Australia/Sydney", + ], +})); + +// Mock the ConductorAutoComplete component to render a simple select +vi.mock("components/v1/ConductorAutoComplete", () => ({ + ConductorAutoComplete: ({ value, onChange, options, ...props }: any) => ( +
    + + +
    + ), +})); + +// Create a test wrapper with theme +const TestWrapper = ({ children }: { children: React.ReactNode }) => { + const theme = createTheme(); + return {children}; +}; + +describe("Schedule Component - TimezonePicker Integration Tests", () => { + it("should render TimezonePicker component (used in Schedule)", () => { + const mockOnChange = vi.fn(); + + render( + + + , + ); + + // Check that the TimezonePicker component renders (this is what Schedule uses) + expect(screen.getByTestId("timezone-picker")).toBeInTheDocument(); + expect(screen.getByTestId("timezone-select")).toBeInTheDocument(); + expect(screen.getByText("Select Timezone")).toBeInTheDocument(); + }); + + it("should handle timezone selection changes (Schedule functionality)", () => { + const mockOnChange = vi.fn(); + + render( + + + , + ); + + const select = screen.getByTestId("timezone-select"); + + // Change the timezone selection (this simulates what happens in Schedule) + fireEvent.change(select, { target: { value: "Europe/London" } }); + + // Verify the onChange was called with the new value + expect(mockOnChange).toHaveBeenCalledWith("Europe/London"); + }); + + it("should work with formatInTimeZone when timezone changes (Schedule integration)", () => { + const mockOnChange = vi.fn(); + const testTime = "2024-01-15T14:30:00Z"; + const formatString = "yyyy-MM-dd HH:mm:ss zzz"; + + render( + + + , + ); + + const select = screen.getByTestId("timezone-select"); + + // Test initial timezone (this is what Schedule does with futureMatches) + expect(() => { + const result = formatInTimeZone(testTime, formatString, "UTC"); + expect(result).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} .+$/); + }).not.toThrow(); + + // Simulate changing to a different timezone (Schedule timezone selection) + fireEvent.change(select, { target: { value: "America/New_York" } }); + + // Test that formatInTimeZone works with the new timezone (Schedule futureMatches display) + expect(() => { + const result = formatInTimeZone( + testTime, + formatString, + "America/New_York", + ); + expect(result).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} .+$/); + // The result should be different due to timezone conversion + expect(result).not.toBe(formatInTimeZone(testTime, formatString, "UTC")); + }).not.toThrow(); + }); + + it("should display all available timezone options (Schedule timezone picker)", () => { + const mockOnChange = vi.fn(); + + render( + + + , + ); + + // Check that all timezone options are available (Schedule timezone selection) + expect(screen.getByRole("option", { name: "UTC" })).toBeInTheDocument(); + expect( + screen.getByRole("option", { name: "America/New_York" }), + ).toBeInTheDocument(); + expect( + screen.getByRole("option", { name: "Europe/London" }), + ).toBeInTheDocument(); + expect( + screen.getByRole("option", { name: "Asia/Tokyo" }), + ).toBeInTheDocument(); + expect( + screen.getByRole("option", { name: "Australia/Sydney" }), + ).toBeInTheDocument(); + }); + + it("should catch the original formatInTimeZone parameter order bug", () => { + const testTime = "2024-01-15T14:30:00Z"; + const timezone = "UTC"; + const formatString = "yyyy-MM-dd HH:mm:ss zzz"; + + // Test correct usage (should work) + expect(() => { + formatInTimeZone(testTime, formatString, timezone); + }).not.toThrow(); + + // Test wrong usage (should throw - this was the original bug) + expect(() => { + formatInTimeZone(testTime, timezone, formatString); // Wrong parameter order + }).toThrow(/Invalid time value/); + }); +}); + +describe("Schedule Component - Cron Expression Validation Tests", () => { + // All cron expression samples from CronExpressionSection.tsx + const cronSamples = [ + { expr: "* * * ? * *", desc: "Every second" }, + { expr: "0 * * ? * *", desc: "Every minute" }, + { expr: "0 */2 * ? * *", desc: "Every 2 minutes" }, + { expr: "0 1/2 * ? * *", desc: "Every 2 minutes starting at minute 1" }, + { expr: "0 */30 * ? * *", desc: "Every 30 minutes" }, + { expr: "0 15,30,45 * ? * *", desc: "At minutes 15, 30, and 45" }, + { expr: "0 0 * ? * *", desc: "Every hour" }, + { expr: "0 0 */2 ? * *", desc: "Every 2 hours" }, + { expr: "0 0 0/2 ? * *", desc: "Every 2 hours starting at midnight" }, + { expr: "0 0 1/2 ? * *", desc: "Every 2 hours starting at 1 AM" }, + { expr: "0 0 0 * * ?", desc: "Daily at midnight" }, + { expr: "0 0 1 * * ?", desc: "Daily at 1 AM" }, + { expr: "0 0 6 * * ?", desc: "Daily at 6 AM" }, + { expr: "0 0 12 ? * SUN", desc: "Every Sunday at noon" }, + { expr: "0 0 12 ? * MON-FRI", desc: "Every weekday at noon" }, + { expr: "0 0 12 ? * SUN,SAT", desc: "Every weekend at noon" }, + { expr: "0 0 12 */7 * ?", desc: "Every 7 days at noon" }, + { expr: "0 0 12 1 * ?", desc: "First day of month at noon" }, + { expr: "0 0 12 15 * ?", desc: "15th day of month at noon" }, + { expr: "0 0 12 1/4 * ?", desc: "Every 4 days starting on 1st at noon" }, + { expr: "0 0 12 L * ?", desc: "Last day of month at noon" }, + { expr: "0 0 12 L-2 * ?", desc: "2 days before last day of month at noon" }, + { expr: "0 0 12 1W * ?", desc: "Nearest weekday to 1st at noon" }, + { expr: "0 0 12 15W * ?", desc: "Nearest weekday to 15th at noon" }, + { expr: "0 0 12 ? * 2#1", desc: "First Monday of month at noon" }, + { expr: "0 0 12 ? * 6#2", desc: "Second Friday of month at noon" }, + { expr: "0 0 12 ? JAN *", desc: "Every day in January at noon" }, + { + expr: "0 0 12 ? JAN,JUN *", + desc: "Every day in January and June at noon", + }, + { + expr: "0 0 12 ? JAN,FEB,APR *", + desc: "Every day in Jan, Feb, Apr at noon", + }, + { expr: "0 0 12 ? 9-12 *", desc: "Every day Sept-Dec at noon" }, + ]; + + it.each(cronSamples)( + "should validate and humanize cron expression: $expr", + ({ expr, desc: _desc }) => { + // Test that expression is valid + const validation = cronExpressionIsValid(expr); + expect(validation.isValid).toBe(true); + expect(validation.errors).toBeNull(); + + // Test that cronstrue can humanize it + expect(() => { + const humanized = cronstrue.toString(expr); + expect(humanized).toBeTruthy(); + expect(typeof humanized).toBe("string"); + }).not.toThrow(); + }, + ); + + it("should specifically validate L-2 pattern (2 days before last day of month)", () => { + const expr = "0 0 12 L-2 * ?"; + + // Validate the expression + const validation = cronExpressionIsValid(expr); + expect(validation.isValid).toBe(true); + expect(validation.errors).toBeNull(); + + // Test humanization + const humanized = cronstrue.toString(expr); + expect(humanized).toBe( + "At 12:00 PM, 2 days before the last day of the month", + ); + }); + + it("should validate various L-n offset patterns", () => { + const offsetPatterns = [ + { expr: "0 0 12 L-1 * ?", offset: 1 }, + { expr: "0 0 12 L-2 * ?", offset: 2 }, + { expr: "0 0 12 L-5 * ?", offset: 5 }, + { expr: "0 0 12 L-10 * ?", offset: 10 }, + ]; + + offsetPatterns.forEach(({ expr, offset }) => { + const validation = cronExpressionIsValid(expr); + expect(validation.isValid).toBe(true); + + const humanized = cronstrue.toString(expr); + // cronstrue doesn't handle singular/plural correctly, so just check for the number + expect(humanized).toContain(`${offset} day`); + expect(humanized).toContain("before the last day of the month"); + }); + }); + + it("should detect L-n pattern correctly", () => { + const hasLOffsetPattern = (cronExpr: string) => /\bL-\d+\b/.test(cronExpr); + + // Should match L-n patterns + expect(hasLOffsetPattern("0 0 12 L-2 * ?")).toBe(true); + expect(hasLOffsetPattern("0 0 12 L-5 * ?")).toBe(true); + expect(hasLOffsetPattern("0 0 12 L-10 * ?")).toBe(true); + + // Should NOT match regular L + expect(hasLOffsetPattern("0 0 12 L * ?")).toBe(false); + + // Should NOT match other patterns + expect(hasLOffsetPattern("0 0 12 1 * ?")).toBe(false); + expect(hasLOffsetPattern("0 0 12 15 * ?")).toBe(false); + expect(hasLOffsetPattern("0 0 12 1W * ?")).toBe(false); + }); + + it("should validate L-n with specific month constraints", () => { + const expr = "0 0 12 L-2 1 ?"; // January only + + const validation = cronExpressionIsValid(expr); + expect(validation.isValid).toBe(true); + + const humanized = cronstrue.toString(expr); + expect(humanized).toContain("2 days before the last day of the month"); + expect(humanized).toContain("January"); + }); + + it("should invalidate malformed cron expressions", () => { + const invalidExpressions = [ + "not a cron", + "* * *", + "0 0 12 L-999 * ?", // offset too large + "invalid pattern", + ]; + + invalidExpressions.forEach((expr) => { + const validation = cronExpressionIsValid(expr); + // Most should be invalid, but we mainly care that the validator doesn't crash + expect(validation).toHaveProperty("isValid"); + expect(validation).toHaveProperty("errors"); + }); + }); +}); + +describe("useCronExpression Hook - Integration Tests", () => { + // All cron expression samples that should work with the hook + const cronSamples = [ + { expr: "* * * ? * *", desc: "Every second" }, + { expr: "0 * * ? * *", desc: "Every minute" }, + { expr: "0 */2 * ? * *", desc: "Every 2 minutes" }, + { expr: "0 1/2 * ? * *", desc: "Every 2 minutes starting at minute 1" }, + { expr: "0 */30 * ? * *", desc: "Every 30 minutes" }, + { expr: "0 15,30,45 * ? * *", desc: "At minutes 15, 30, and 45" }, + { expr: "0 0 * ? * *", desc: "Every hour" }, + { expr: "0 0 */2 ? * *", desc: "Every 2 hours" }, + { expr: "0 0 0/2 ? * *", desc: "Every 2 hours starting at midnight" }, + { expr: "0 0 1/2 ? * *", desc: "Every 2 hours starting at 1 AM" }, + { expr: "0 0 0 * * ?", desc: "Daily at midnight" }, + { expr: "0 0 1 * * ?", desc: "Daily at 1 AM" }, + { expr: "0 0 6 * * ?", desc: "Daily at 6 AM" }, + { expr: "0 0 12 ? * SUN", desc: "Every Sunday at noon" }, + { expr: "0 0 12 ? * MON-FRI", desc: "Every weekday at noon" }, + { expr: "0 0 12 ? * SUN,SAT", desc: "Every weekend at noon" }, + { expr: "0 0 12 */7 * ?", desc: "Every 7 days at noon" }, + { expr: "0 0 12 1 * ?", desc: "First day of month at noon" }, + { expr: "0 0 12 15 * ?", desc: "15th day of month at noon" }, + { expr: "0 0 12 1/4 * ?", desc: "Every 4 days starting on 1st at noon" }, + { expr: "0 0 12 L * ?", desc: "Last day of month at noon" }, + { + expr: "0 0 12 L-2 * ?", + desc: "2 days before last day of month at noon", + isLOffset: true, + }, + { expr: "0 0 12 1W * ?", desc: "Nearest weekday to 1st at noon" }, + { expr: "0 0 12 15W * ?", desc: "Nearest weekday to 15th at noon" }, + { expr: "0 0 12 ? * 2#1", desc: "First Monday of month at noon" }, + { expr: "0 0 12 ? * 6#2", desc: "Second Friday of month at noon" }, + { expr: "0 0 12 ? JAN *", desc: "Every day in January at noon" }, + { + expr: "0 0 12 ? JAN,JUN *", + desc: "Every day in January and June at noon", + }, + { + expr: "0 0 12 ? JAN,FEB,APR *", + desc: "Every day in Jan, Feb, Apr at noon", + }, + { expr: "0 0 12 ? 9-12 *", desc: "Every day Sept-Dec at noon" }, + ]; + + it.each(cronSamples)( + "useCronExpression hook should work for: $expr", + ({ expr, desc: _desc, isLOffset }) => { + const { result } = renderHook(() => useCronExpression(expr, "UTC")); + + // Hook should not have errors + expect(result.current.cronError).toBeUndefined(); + + // Should return the expression + expect(result.current.cronExpression).toBe(expr); + + // Should have a humanized expression + expect(result.current.humanizedExpression).toBeTruthy(); + expect(typeof result.current.humanizedExpression).toBe("string"); + + // Should have futureMatches array (may be empty or populated) + expect(Array.isArray(result.current.futureMatches)).toBe(true); + + // For L-offset patterns, we use custom calculator which should return matches + expect( + !isLOffset || result.current.futureMatches.length > 0, + ).toBeTruthy(); + }, + ); + + it("should handle L-2 pattern with custom calculator in UTC", () => { + const { result } = renderHook(() => + useCronExpression("0 0 12 L-2 * ?", "UTC"), + ); + + // Should not have errors + expect(result.current.cronError).toBeUndefined(); + + // Should have humanized expression + expect(result.current.humanizedExpression).toBe( + "At 12:00 PM, 2 days before the last day of the month", + ); + + // Should have future matches calculated by custom function + expect(result.current.futureMatches.length).toBeGreaterThan(0); + + // Validate each match is actually 2 days before the last day of the month + result.current.futureMatches.forEach((match) => { + // Date string format: "yyyy-MM-dd HH:mm:ss" + expect(match).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/); + + // Parse the date components directly from the string + const [datePart, timePart] = match.split(" "); + const [year, month, day] = datePart.split("-").map(Number); + const [hours, minutes, seconds] = timePart.split(":").map(Number); + + // Calculate the last day of that month (month is 1-indexed in the string) + const lastDayOfMonth = new Date(year, month, 0).getDate(); // month 0 = last day of previous month + const expectedDay = lastDayOfMonth - 2; // L-2 means 2 days before last + + // Verify the match is on the correct day + expect(day).toBe(expectedDay); + + // Verify the time is 12:00:00 (from "0 0 12" in cron) + expect(hours).toBe(12); + expect(minutes).toBe(0); + expect(seconds).toBe(0); + }); + }); + + it("should handle L-2 pattern with custom calculator in America/New_York", () => { + const { result } = renderHook(() => + useCronExpression("0 0 14 L-2 * ?", "America/New_York"), + ); + + // Should not have errors + expect(result.current.cronError).toBeUndefined(); + + // Should have humanized expression + expect(result.current.humanizedExpression).toBe( + "At 02:00 PM, 2 days before the last day of the month", + ); + + // Should have future matches calculated by custom function + expect(result.current.futureMatches.length).toBeGreaterThan(0); + + // Validate each match is actually 2 days before the last day of the month + result.current.futureMatches.forEach((match) => { + // Date string format: "yyyy-MM-dd HH:mm:ss" + expect(match).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/); + + // Parse the date components directly from the string + const [datePart, timePart] = match.split(" "); + const [year, month, day] = datePart.split("-").map(Number); + const [hours, minutes, seconds] = timePart.split(":").map(Number); + + // Calculate the last day of that month (month is 1-indexed in the string) + const lastDayOfMonth = new Date(year, month, 0).getDate(); + const expectedDay = lastDayOfMonth - 2; // L-2 means 2 days before last + + // Verify the match is on the correct day + expect(day).toBe(expectedDay); + + // Note: The cron time (14:00 UTC) is converted to America/New_York timezone (UTC-5) + // 14:00 UTC = 09:00 EST (or 10:00 EDT depending on DST) + // We expect 9 or 10 hours depending on daylight saving time + expect([9, 10]).toContain(hours); + expect(minutes).toBe(0); + expect(seconds).toBe(0); + }); + }); + + it("should handle L-2 pattern with custom calculator in Asia/Tokyo", () => { + const { result } = renderHook(() => + useCronExpression("0 30 9 L-2 * ?", "Asia/Tokyo"), + ); + + // Should not have errors + expect(result.current.cronError).toBeUndefined(); + + // Should have humanized expression + expect(result.current.humanizedExpression).toBe( + "At 09:30 AM, 2 days before the last day of the month", + ); + + // Should have future matches calculated by custom function + expect(result.current.futureMatches.length).toBeGreaterThan(0); + + // Validate each match is actually 2 days before the last day of the month + result.current.futureMatches.forEach((match) => { + // Date string format: "yyyy-MM-dd HH:mm:ss" + expect(match).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/); + + // Parse the date components directly from the string + const [datePart, timePart] = match.split(" "); + const [year, month, day] = datePart.split("-").map(Number); + const [hours, minutes, seconds] = timePart.split(":").map(Number); + + // Calculate the last day of that month (month is 1-indexed in the string) + const lastDayOfMonth = new Date(year, month, 0).getDate(); + const expectedDay = lastDayOfMonth - 2; // L-2 means 2 days before last + + // Verify the match is on the correct day + expect(day).toBe(expectedDay); + + // Note: The cron time (09:30 UTC) is converted to Asia/Tokyo timezone (UTC+9) + // 09:30 UTC = 18:30 JST (Japan Standard Time, no DST) + expect(hours).toBe(18); + expect(minutes).toBe(30); + expect(seconds).toBe(0); + }); + }); + + it("should handle multiple L-n offset patterns", () => { + const offsets = [ + { expr: "0 0 12 L-1 * ?", offset: 1 }, + { expr: "0 0 12 L-2 * ?", offset: 2 }, + { expr: "0 0 12 L-5 * ?", offset: 5 }, + { expr: "0 0 12 L-10 * ?", offset: 10 }, + ]; + + offsets.forEach(({ expr, offset }) => { + const { result } = renderHook(() => useCronExpression(expr, "UTC")); + + expect(result.current.cronError).toBeUndefined(); + expect(result.current.futureMatches.length).toBeGreaterThan(0); + + // Verify each match is correct for the specific offset + result.current.futureMatches.forEach((match) => { + // Date string format: "yyyy-MM-dd HH:mm:ss" + expect(match).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/); + + // Parse the date components directly from the string + const [datePart, timePart] = match.split(" "); + const [year, month, day] = datePart.split("-").map(Number); + const [hours, minutes, seconds] = timePart.split(":").map(Number); + + // Calculate the last day of that month (month is 1-indexed in the string) + const lastDayOfMonth = new Date(year, month, 0).getDate(); + const expectedDay = lastDayOfMonth - offset; + + // Verify the match is on the correct day (offset days before last) + expect(day).toBe(expectedDay); + + // Verify the time is 12:00:00 + expect(hours).toBe(12); + expect(minutes).toBe(0); + expect(seconds).toBe(0); + }); + }); + }); + + it("should update when timezone changes", () => { + const { result, rerender } = renderHook( + ({ timezone }) => useCronExpression("0 0 12 L-2 * ?", timezone), + { initialProps: { timezone: "UTC" } }, + ); + + const utcMatches = result.current.futureMatches; + expect(utcMatches.length).toBeGreaterThan(0); + + // Change timezone + rerender({ timezone: "America/New_York" }); + + // Should still have matches + expect(result.current.futureMatches.length).toBeGreaterThan(0); + + // Matches might be different due to timezone + expect(result.current.futureMatches).toBeDefined(); + }); + + it("should handle setCronExpression to update expression", () => { + const { result } = renderHook(() => + useCronExpression("0 0 12 L * ?", "UTC"), + ); + + expect(result.current.cronExpression).toBe("0 0 12 L * ?"); + expect(result.current.cronError).toBeUndefined(); + + // Update to L-2 pattern + result.current.setCronExpression("0 0 12 L-2 * ?", "UTC"); + + // Wait for state update + waitFor(() => { + expect(result.current.cronExpression).toBe("0 0 12 L-2 * ?"); + expect(result.current.cronError).toBeUndefined(); + expect(result.current.futureMatches.length).toBeGreaterThan(0); + }); + }); + + it("should return error for invalid expression", () => { + let errorReceived: string | undefined; + const onError = (error: string | undefined) => { + errorReceived = error; + }; + + const { result } = renderHook(() => + useCronExpression("invalid cron", "UTC", onError), + ); + + // Should have an error (can be string or object/array) + expect(result.current.cronError).toBeDefined(); + expect(result.current.cronError).toBeTruthy(); + + // Error callback should be called + waitFor(() => { + expect(errorReceived).toBeDefined(); + }); + }); + + it("should handle regular L pattern with cronjs-matcher (not custom calculator)", () => { + const { result } = renderHook(() => + useCronExpression("0 0 12 L * ?", "UTC"), + ); + + // Should not have errors + expect(result.current.cronError).toBeUndefined(); + + // Should use cronjs-matcher (not custom calculator) + // This will succeed or have empty matches depending on cronjs-matcher support + expect(Array.isArray(result.current.futureMatches)).toBe(true); + + // Should have humanized expression + expect(result.current.humanizedExpression).toBe( + "At 12:00 PM, on the last day of the month", + ); + }); + + it("should provide highlightedPart controls", () => { + const { result } = renderHook(() => + useCronExpression("0 0 12 L-2 * ?", "UTC"), + ); + + expect(result.current.highlightedPart).toBeNull(); + + // Set highlighted part + result.current.setHighlightedPart(2); + + waitFor(() => { + expect(result.current.highlightedPart).toBe(2); + }); + }); + + it("should return error when L-offset pattern fails to calculate matches", () => { + // Use an invalid L-offset pattern that can't be parsed + const { result } = renderHook(() => + useCronExpression("0 0 12 L-999 * ?", "UTC"), + ); + + // Should have an error - either from validation or match calculation + expect(result.current.cronError).toBeDefined(); + expect(result.current.cronError).toBeTruthy(); + + // Future matches should be empty since calculation failed - if empty, error must be defined + expect( + result.current.futureMatches.length > 0 || result.current.cronError, + ).toBeTruthy(); + }); + + it("should handle expressions that pass validation but fail future match calculation", () => { + let errorReceived: string | undefined; + const onError = (error: string | undefined) => { + errorReceived = error; + }; + + // Use an expression that might validate but can't calculate future matches + // For example, an extremely complex pattern + const { result } = renderHook(() => + useCronExpression("0 0 12 ? * MON#5", "UTC", onError), + ); + + // If future matches fail to calculate, there should be an error + // Assert that either we have matches or an error was received + waitFor(() => { + expect( + result.current.futureMatches.length > 0 || errorReceived !== undefined, + ).toBeTruthy(); + }); + }); +}); diff --git a/ui-next/src/pages/scheduler/components/CronExpressionSection.tsx b/ui-next/src/pages/scheduler/components/CronExpressionSection.tsx new file mode 100644 index 0000000000..d380cd03ce --- /dev/null +++ b/ui-next/src/pages/scheduler/components/CronExpressionSection.tsx @@ -0,0 +1,329 @@ +import { + Box, + Grid, + MenuItem, + Paper, + SxProps, + Theme, + useMediaQuery, +} from "@mui/material"; +import { Text } from "components"; +import MuiTypography from "components/MuiTypography"; +import ConductorInput from "components/v1/ConductorInput"; +import ConductorSelect from "components/v1/ConductorSelect"; +import cronstrue from "cronstrue"; +import { + formatInTimeZone, + guessUserTimeZone, + parseDateInTimeZone, +} from "utils/date"; +import { CRON_COLORS_BY_POSITION } from "../constants"; +import CronExpressionHelp from "../CronExpressionHelp"; +import { TimezonePicker } from "../TimezonePicker"; + +const cronSamples = [ + "* * * ? * *", + "0 * * ? * *", + "0 */2 * ? * *", + "0 1/2 * ? * *", + "0 */30 * ? * *", + "0 15,30,45 * ? * *", + "0 0 * ? * *", + "0 0 */2 ? * *", + "0 0 0/2 ? * *", + "0 0 1/2 ? * *", + "0 0 0 * * ?", + "0 0 1 * * ?", + "0 0 6 * * ?", + "0 0 12 ? * SUN", + "0 0 12 ? * MON-FRI", + "0 0 12 ? * SUN,SAT", + "0 0 12 */7 * ?", + "0 0 12 1 * ?", + "0 0 12 15 * ?", + "0 0 12 1/4 * ?", + "0 0 12 L * ?", + "0 0 12 L-2 * ?", + "0 0 12 1W * ?", + "0 0 12 15W * ?", + "0 0 12 ? * 2#1", + "0 0 12 ? * 6#2", + "0 0 12 ? JAN *", + "0 0 12 ? JAN,JUN *", + "0 0 12 ? JAN,FEB,APR *", + "0 0 12 ? 9-12 *", +]; + +const utcWinWidth = "180px"; +const browserTimeMinWidth = "230px"; + +interface CronExpressionSectionProps { + cronExpression: string; + setCronExpression: (value: string, timezone: string) => void; + futureMatches: string[]; + humanizedExpression: string; + highlightedPart: number | null; + getHighlightedPart: (value: string, selectionStart: number) => void; + setHighlightedPart: (part: number | null) => void; + selectedTemplate: string; + setSelectedTemplate: (template: string) => void; + timezone: string; + setZoneId: (value: string) => void; + cronError?: string; + minWidthCronExpression: string; +} + +export function CronExpressionSection({ + cronExpression, + setCronExpression, + futureMatches, + humanizedExpression, + highlightedPart, + getHighlightedPart, + setHighlightedPart, + selectedTemplate, + setSelectedTemplate, + timezone, + setZoneId, + cronError, + minWidthCronExpression, +}: CronExpressionSectionProps) { + const isMDWidth = useMediaQuery((theme: Theme) => theme.breakpoints.up("md")); + + const timeListStyle: SxProps = { + flexWrap: isMDWidth ? "nowrap" : "wrap", + justifyContent: "start", + }; + + return ( + + + + + + Cron Expressions Help + + + + { + setCronExpression(e.target.value, timezone); + setSelectedTemplate(e.target.value); + }} + value={selectedTemplate} + sx={{ + ".MuiInputBase-root": { + ".MuiSelect-select": { + minHeight: "2.7em", + }, + }, + }} + > + {cronSamples && + cronSamples.map((cs, i) => { + return ( + + + + {cs.split(" ").map((cronExpressionFragment, index) => { + return ( + + {cronExpressionFragment} + + ); + })} + + + {cronstrue.toString(cs)} + + + + ); + })} + + + + + + + setCronExpression(value, timezone) + } + onKeyDown={(e: any) => { + getHighlightedPart(e.target.value, e.target.selectionStart); + }} + onKeyUp={(e: any) => { + getHighlightedPart(e.target.value, e.target.selectionStart); + }} + onClick={(e: any) => { + getHighlightedPart(e.target.value, e.target.selectionStart); + }} + onBlur={(_e) => { + setHighlightedPart(null); + }} + error={cronError !== undefined} + helperText={cronError} + inputProps={{ + sx: { + fontSize: "1.3rem", + }, + }} + /> + + { + setZoneId(value); + setCronExpression(cronExpression, value); + }} + /> + + + + {futureMatches && ( + + + Next run schedules based on the expression: + + {cronExpression && ( + + {humanizedExpression} ({timezone}) + + )} + {futureMatches && futureMatches.length === 0 && ( + No schedules possible + )} + {futureMatches?.length > 0 && ( + + + + {timezone} Time + + {futureMatches.map((time) => { + const parsed = parseDateInTimeZone(time, timezone); + const formatted = formatInTimeZone( + parsed, + "yyyy-MM-dd HH:mm:ss zzz", + timezone, + ); + + return ( + + {formatted} + + ); + })} + + + + + Browser local time + + {futureMatches.map((time) => { + const browserTz = guessUserTimeZone(); + const formatted = formatInTimeZone( + new Date(time), + "yyyy-MM-dd HH:mm:ss zzz", + browserTz, + ); + + return ( + + {formatted} + + ); + })} + + + )} + + )} + + + + + + ); +} diff --git a/ui-next/src/pages/scheduler/components/ScheduleTimingSection.tsx b/ui-next/src/pages/scheduler/components/ScheduleTimingSection.tsx new file mode 100644 index 0000000000..16807aa2c5 --- /dev/null +++ b/ui-next/src/pages/scheduler/components/ScheduleTimingSection.tsx @@ -0,0 +1,81 @@ +import { + FormControl, + FormControlLabel, + Grid, + InputLabel, + Switch, + Tooltip, +} from "@mui/material"; +import { ConductorCodeBlockInput } from "components/v1/ConductorCodeBlockInput"; +import ConductorDateRangePicker from "components/v1/date-time/ConductorDateRangePicker"; +import { baseLabelStyle } from "components/v1/theme/styles"; +import { SMALL_EDITOR_DEFAULT_OPTIONS } from "utils/constants"; + +interface ScheduleTimingSectionProps { + scheduleStartTime: string | number; + scheduleEndTime: string | number; + handleScheduleStartTime: (value: number) => void; + handleScheduleEndTime: (value: number) => void; + taskToDomain: string; + setWorkflowTasksToDomainState: (value: string) => void; + paused: boolean; + setCronPausedState: () => void; +} + +export function ScheduleTimingSection({ + scheduleStartTime, + scheduleEndTime, + handleScheduleStartTime, + handleScheduleEndTime, + taskToDomain, + setWorkflowTasksToDomainState, + paused, + setCronPausedState, +}: ScheduleTimingSectionProps) { + return ( + <> + + + + + + + + + Start schedule paused? + + + setCronPausedState()} + /> + } + label="Pause schedule" + sx={{ mt: 3, mb: 3 }} + /> + + + + ); +} diff --git a/ui-next/src/pages/scheduler/components/WorkflowConfigSection.tsx b/ui-next/src/pages/scheduler/components/WorkflowConfigSection.tsx new file mode 100644 index 0000000000..914cc14d48 --- /dev/null +++ b/ui-next/src/pages/scheduler/components/WorkflowConfigSection.tsx @@ -0,0 +1,101 @@ +import { Grid } from "@mui/material"; +import { ConductorAutoComplete } from "components/v1"; +import { ConductorCodeBlockInput } from "components/v1/ConductorCodeBlockInput"; +import ConductorInput from "components/v1/ConductorInput"; +import { SMALL_EDITOR_DEFAULT_OPTIONS } from "utils/constants"; +import { IdempotencyValuesProp } from "../../definition/RunWorkflow/state"; +import IdempotencyForm from "../../runWorkflow/IdempotencyForm"; + +interface WorkflowConfigSectionProps { + workflowType: string | null; + setWorkflowType: (workflowType: string) => void; + workflowVersion: string | null; + setWorkflowVersion: (workflowVersion: string | null) => void; + workflowVersions: string[]; + workflowNames: string[]; + workflowInputTemplate: string; + setWorkflowInputTemplate: (value: string) => void; + workflowCorrelationId: string; + setWorkflowCorrelationId: (value: string) => void; + idempotencyValues: { + idempotencyKey?: string; + idempotencyStrategy?: any; + }; + handleIdempotencyValues: (data: IdempotencyValuesProp) => void; + errors?: any; +} + +export function WorkflowConfigSection({ + workflowType, + setWorkflowType, + workflowVersion, + setWorkflowVersion, + workflowVersions, + workflowNames, + workflowInputTemplate, + setWorkflowInputTemplate, + workflowCorrelationId, + setWorkflowCorrelationId, + idempotencyValues, + handleIdempotencyValues, + errors, +}: WorkflowConfigSectionProps) { + return ( + <> + + setWorkflowType(val)} + value={workflowType} + error={errors?.["startWorkflowRequest.name"]} + helperText={errors ? errors["startWorkflowRequest.name"] : undefined} + /> + + + setWorkflowVersion(val)} + value={workflowVersion === "" ? "Latest version" : workflowVersion} + conductorInputProps={{ + tooltip: { + title: "Workflow version", + content: "Optional, by default the latest version is triggered", + }, + }} + /> + + + + + + + + + + + + + + ); +} diff --git a/ui-next/src/pages/scheduler/constants.ts b/ui-next/src/pages/scheduler/constants.ts new file mode 100644 index 0000000000..7ceb96998e --- /dev/null +++ b/ui-next/src/pages/scheduler/constants.ts @@ -0,0 +1,8 @@ +export const CRON_COLORS_BY_POSITION = [ + "#4FAAD1", + "#6569AC", + "#45AC59", + "#C99E00", + "#EE6B31", + "#CE2836", +]; diff --git a/ui-next/src/pages/scheduler/hooks/useCronExpression.ts b/ui-next/src/pages/scheduler/hooks/useCronExpression.ts new file mode 100644 index 0000000000..0cbe6e97ef --- /dev/null +++ b/ui-next/src/pages/scheduler/hooks/useCronExpression.ts @@ -0,0 +1,231 @@ +import * as cronjsMatcher from "@datasert/cronjs-matcher"; +import cronstrue from "cronstrue"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { cronExpressionIsValid } from "utils/cronHelpers"; +import { formatInTimeZone } from "utils/date"; + +export interface UseCronExpressionReturn { + cronExpression: string; + setCronExpression: (value: string, timezone: string) => void; + futureMatches: string[]; + humanizedExpression: string; + cronError: string | undefined; + highlightedPart: number | null; + setHighlightedPart: (part: number | null) => void; +} + +const resolveTimezone = (value?: string) => value || "UTC"; + +/** + * Calculate future matches for cron expressions with L-n pattern (e.g., L-2 for 2 days before last day of month) + * since JavaScript cron libraries don't support this Quartz syntax + */ +function calculateLastDayOffsetMatches( + cronExpression: string, + timezone: string, + count: number = 10, +): string[] { + // Parse cron expression: seconds minutes hours dayOfMonth month dayOfWeek + const parts = cronExpression.trim().split(/\s+/); + if (parts.length < 6) { + return []; + } + + const [seconds, minutes, hours, dayOfMonth, month, dayOfWeek] = parts; + + // Extract offset from L-n pattern + const match = dayOfMonth.match(/^L-(\d+)$/); + if (!match) { + return []; + } + + const offset = parseInt(match[1], 10); + const matches: string[] = []; + const now = new Date(); + // eslint-disable-next-line prefer-const + let currentDate = new Date(now); + + // Look ahead up to 24 months to find matches + let monthsChecked = 0; + while (matches.length < count && monthsChecked < 24) { + const year = currentDate.getFullYear(); + const monthNum = currentDate.getMonth(); + + // Get last day of month + const lastDayOfMonth = new Date(year, monthNum + 1, 0); + const targetDay = lastDayOfMonth.getDate() - offset; + + if (targetDay > 0) { + // Set time from cron expression + const hour = hours === "*" || hours === "?" ? 0 : parseInt(hours, 10); + const minute = + minutes === "*" || minutes === "?" ? 0 : parseInt(minutes, 10); + const second = + seconds === "*" || seconds === "?" ? 0 : parseInt(seconds, 10); + + // Create potential match date in UTC + const matchDate = new Date( + Date.UTC(year, monthNum, targetDay, hour, minute, second), + ); + + // Only include if it's in the future + if (matchDate > now) { + // Check day of week constraint if specified + if (dayOfWeek !== "*" && dayOfWeek !== "?") { + const dow = matchDate.getDay(); // 0 = Sunday + const expectedDow = parseInt(dayOfWeek, 10); + if (dow !== expectedDow) { + currentDate.setMonth(currentDate.getMonth() + 1); + monthsChecked++; + continue; + } + } + + // Check month constraint if specified + if (month !== "*" && month !== "?") { + const expectedMonth = parseInt(month, 10); + if (monthNum + 1 !== expectedMonth) { + currentDate.setMonth(currentDate.getMonth() + 1); + monthsChecked++; + continue; + } + } + + // Format in the specified timezone + const formatted = formatInTimeZone( + matchDate, + "yyyy-MM-dd HH:mm:ss", + timezone, + ); + matches.push(formatted); + } + } + + // Move to next month + currentDate.setMonth(currentDate.getMonth() + 1); + monthsChecked++; + } + + return matches; +} + +export function useCronExpression( + initialCronExpression: string = "", + timezone: string = "UTC", + onError?: (error: string | undefined) => void, +): UseCronExpressionReturn { + const [cronExpression, setCronExpressionState] = useState( + initialCronExpression, + ); + const [activeTimezone, setActiveTimezone] = useState(() => + resolveTimezone(timezone), + ); + const [highlightedPart, setHighlightedPart] = useState(null); + + // Sync with props changes + const prevInitialRef = useRef(initialCronExpression); + const prevTimezoneRef = useRef(timezone); + + if ( + initialCronExpression !== prevInitialRef.current || + timezone !== prevTimezoneRef.current + ) { + setCronExpressionState(initialCronExpression); + setActiveTimezone(resolveTimezone(timezone)); + prevInitialRef.current = initialCronExpression; + prevTimezoneRef.current = timezone; + } + + const validation = useMemo(() => { + if (!cronExpression.trim()) { + return { + matches: [] as string[], + humanized: "", + error: undefined, + }; + } + + try { + const validation = cronExpressionIsValid(cronExpression); + if (!validation.isValid) { + return { + matches: [], + humanized: "", + error: validation.errors || "Invalid cron expression", + }; + } + + // Check if expression contains Quartz L-n offset pattern (e.g., L-2) + // JavaScript cron libraries don't support this, so we calculate manually + const hasLastDayOffset = /\bL-\d+\b/.test(cronExpression); + + let matches: string[] = []; + let matchError: string | undefined; + + if (hasLastDayOffset) { + try { + matches = calculateLastDayOffsetMatches( + cronExpression, + activeTimezone || "UTC", + 10, + ); + if (matches.length === 0) { + matchError = + "Unable to calculate future matches for this expression"; + } + } catch { + matchError = + "Failed to calculate schedule times. Please verify the expression."; + } + } else { + try { + matches = cronjsMatcher.getFutureMatches(cronExpression, { + hasSeconds: true, + timezone: activeTimezone || "UTC", + }); + } catch { + // If getFutureMatches fails, the expression likely won't work + matchError = + "Unable to calculate future matches. This expression may not be supported."; + } + } + + return { + matches: matches.length > 0 ? matches : [], + humanized: cronstrue.toString(cronExpression), + error: matchError, + }; + } catch { + return { + matches: [], + humanized: "", + error: "Invalid cron expression format", + }; + } + }, [cronExpression, activeTimezone]); + + // Use ref to avoid re-triggering effect when onError changes + const onErrorRef = useRef(onError); + useEffect(() => { + onErrorRef.current = onError; + }); + + useEffect(() => { + onErrorRef.current?.(validation.error); + }, [validation.error]); + + const setCronExpression = useCallback((value: string, tz: string) => { + setCronExpressionState(value); + setActiveTimezone(resolveTimezone(tz)); + }, []); + + return { + cronExpression, + setCronExpression, + futureMatches: validation.matches, + humanizedExpression: validation.humanized, + cronError: validation.error, + highlightedPart, + setHighlightedPart, + }; +} diff --git a/ui-next/src/pages/scheduler/hooks/useScheduleFormHandlers.ts b/ui-next/src/pages/scheduler/hooks/useScheduleFormHandlers.ts new file mode 100644 index 0000000000..035244a6f6 --- /dev/null +++ b/ui-next/src/pages/scheduler/hooks/useScheduleFormHandlers.ts @@ -0,0 +1,193 @@ +import React, { useCallback } from "react"; +import { IdempotencyValuesProp } from "../../definition/RunWorkflow/state"; +import { IdempotencyStrategyEnum } from "../../runWorkflow/types"; +import { ScheduleType } from "../Schedule"; + +export interface UseScheduleFormHandlersReturn { + setScheduleNewState: (key: string, value: string) => void; + setZoneId: (value: string) => void; + setCronPausedState: () => void; + setWorkflowInputTemplatesState: (value: string) => void; + setWorkflowTasksToDomainState: (value: string) => void; + setWorkflowCorrelationIdState: (value: string) => void; + handleIdempotencyValues: (data: IdempotencyValuesProp) => void; + handleScheduleStartTime: (value: number) => void; + handleScheduleEndTime: (value: number) => void; + getHighlightedPart: (value: string, selectionStart: number) => void; +} + +const scheduleNamePattern = /^[a-zA-Z0-9_]+$/; + +export function useScheduleFormHandlers( + scheduleState: ScheduleType, + setScheduleState: React.Dispatch>, + setErrors: React.Dispatch>, + clearError: (field: string) => void, + errors: any, + setCouldNotParseJson: (value: boolean) => void, + setHighlightedPart: (part: number | null) => void, +): UseScheduleFormHandlersReturn { + const setScheduleNewState = useCallback( + (key: string, value: string) => { + // Validate name field + if (key === "name") { + if (!value.trim()) { + // Set error for empty name + setErrors((prevErrors: any) => ({ + ...prevErrors, + name: "Name is required", + })); + } else if (!scheduleNamePattern.test(value)) { + // Set error for invalid name pattern + setErrors((prevErrors: any) => ({ + ...prevErrors, + name: "Name can only contain letters, numbers, and underscores.", + })); + } else { + // Clear error if name is valid + if (errors?.name) { + clearError("name"); + } + } + } else { + // For other fields, just clear error if it exists + if (errors?.[key]) { + clearError(key); + } + } + + setScheduleState((prevState) => ({ + ...prevState, + [key]: value, + })); + }, + [setScheduleState, setErrors, clearError, errors], + ); + + const setZoneId = useCallback( + (value: string) => { + if (errors?.zoneId) { + clearError("zoneId"); + } + setScheduleState((prevState) => ({ + ...prevState, + zoneId: value, + })); + }, + [setScheduleState, clearError, errors], + ); + + const setCronPausedState = useCallback(() => { + setScheduleState((prevState) => ({ + ...prevState, + paused: !prevState.paused, + })); + }, [setScheduleState]); + + const setWorkflowInputTemplatesState = useCallback( + (value: string) => { + try { + JSON.parse(value); + setCouldNotParseJson(false); + } catch { + setCouldNotParseJson(true); + return; + } + setScheduleState((prevState) => ({ + ...prevState, + workflowInputTemplate: value, + })); + }, + [setScheduleState, setCouldNotParseJson], + ); + + const setWorkflowTasksToDomainState = useCallback( + (value: string) => { + try { + JSON.parse(value); + setCouldNotParseJson(false); + } catch { + setCouldNotParseJson(true); + return; + } + setScheduleState((prevState) => ({ + ...prevState, + taskToDomain: value, + })); + }, + [setScheduleState, setCouldNotParseJson], + ); + + const setWorkflowCorrelationIdState = useCallback( + (value: string) => { + setScheduleState((prevState) => ({ + ...prevState, + workflowCorrelationId: value, + })); + }, + [setScheduleState], + ); + + const handleIdempotencyValues = useCallback( + (data: IdempotencyValuesProp) => { + const idempotencyStrategy = () => { + if (data.idempotencyStrategy) { + return data.idempotencyStrategy; + } + if (scheduleState?.workflowIdempotencyStrategy) { + return scheduleState?.workflowIdempotencyStrategy; + } + return IdempotencyStrategyEnum.RETURN_EXISTING; + }; + setScheduleState((prevState) => ({ + ...prevState, + workflowIdempotencyKey: data?.idempotencyKey, + workflowIdempotencyStrategy: data?.idempotencyKey + ? idempotencyStrategy() + : undefined, + })); + }, + [scheduleState, setScheduleState], + ); + + const handleScheduleStartTime = useCallback( + (value: number) => { + setScheduleState((prevState) => ({ + ...prevState, + scheduleStartTime: value, + })); + }, + [setScheduleState], + ); + + const handleScheduleEndTime = useCallback( + (value: number) => { + setScheduleState((prevState) => ({ + ...prevState, + scheduleEndTime: value, + })); + }, + [setScheduleState], + ); + + const getHighlightedPart = useCallback( + (value: string, selectionStart: number) => { + const partsUntilCursor = value.substring(0, selectionStart).split(" "); + setHighlightedPart(partsUntilCursor.length - 1); + }, + [setHighlightedPart], + ); + + return { + setScheduleNewState, + setZoneId, + setCronPausedState, + setWorkflowInputTemplatesState, + setWorkflowTasksToDomainState, + setWorkflowCorrelationIdState, + handleIdempotencyValues, + handleScheduleStartTime, + handleScheduleEndTime, + getHighlightedPart, + }; +} diff --git a/ui-next/src/pages/scheduler/hooks/useScheduleState.ts b/ui-next/src/pages/scheduler/hooks/useScheduleState.ts new file mode 100644 index 0000000000..2e70a43ded --- /dev/null +++ b/ui-next/src/pages/scheduler/hooks/useScheduleState.ts @@ -0,0 +1,174 @@ +import React, { useMemo, useState } from "react"; +import { timestampRendererLocal } from "utils/date"; +import { getTemplateFromInputParams } from "../../runWorkflow/runWorkflowUtils"; +import { ScheduleType } from "../Schedule"; + +export interface UseScheduleStateReturn { + scheduleState: ScheduleType; + setScheduleState: React.Dispatch>; + original: Partial; + setOriginal: React.Dispatch>>; + initializeFromSchedule: (schedule: any) => void; + initializeFromExecution: (latestExecution: any) => void; +} + +const initialState: ScheduleType = { + name: "", + description: "", + cronExpression: "", + paused: false, + runCatchupScheduleInstances: false, + workflowType: null, + workflowVersion: null, + workflowVersions: [], + workflowInputTemplate: "", + taskToDomain: "", + workflowCorrelationId: "", + workflowIdempotencyKey: undefined, + workflowIdempotencyStrategy: undefined, + workflowDef: null, + externalInputPayloadStoragePath: undefined, + scheduleStartTime: "", + scheduleEndTime: "", + priority: "", + zoneId: "UTC", +}; + +export function useScheduleState( + latestExecution: any, + _schedule: any, +): UseScheduleStateReturn { + const memorizedState = useMemo( + () => ({ + ...initialState, + workflowType: latestExecution?.workflowName || null, + workflowVersion: latestExecution?.workflowVersion + ? `${latestExecution?.workflowVersion}` + : null, + workflowInputTemplate: + latestExecution?.workflowDefinition?.inputParameters && + latestExecution.workflowDefinition.inputParameters.length > 0 + ? getTemplateFromInputParams( + latestExecution?.workflowDefinition?.inputParameters, + ) + : "", + taskToDomain: latestExecution?.taskToDomain + ? JSON.stringify(latestExecution.taskToDomain, null, 2) + : "", + }), + [latestExecution], + ); + + const [scheduleState, setScheduleState] = + useState(memorizedState); + const [original, setOriginal] = useState>({ + paused: false, + runCatchupScheduleInstances: false, + name: "", + description: "", + cronExpression: "", + scheduleStartTime: "", + scheduleEndTime: "", + zoneId: "UTC", + startWorkflowRequest: { + name: null, + version: null, + input: {}, + correlationId: "", + taskToDomain: {}, + priority: "", + }, + }); + + const initializeFromSchedule = useMemo( + () => (schedule: any) => { + if (!schedule) return; + + const swr = schedule.startWorkflowRequest || {}; + const workflowInput = swr.input ? JSON.stringify(swr.input, null, 2) : ""; + const taskToDomainStr = swr.taskToDomain + ? JSON.stringify(swr.taskToDomain, null, 2) + : ""; + let cronExpression = schedule.cronExpression; + if (cronExpression === null) { + cronExpression = ""; + } + + const newState = { + name: schedule.name, + description: schedule.description || "", + cronExpression: cronExpression, + runCatchupScheduleInstances: schedule.runCatchupScheduleInstances, + paused: schedule.paused, + workflowType: swr.name, + workflowVersions: [], // Will be set by workflow config hook + workflowVersion: swr.version ? `${swr.version}` : "", + workflowCorrelationId: swr.correlationId, + workflowIdempotencyKey: swr?.idempotencyKey, + workflowIdempotencyStrategy: swr?.idempotencyStrategy, + workflowInputTemplate: workflowInput, + taskToDomain: taskToDomainStr, + workflowDef: JSON.stringify(swr.workflowDef), + externalInputPayloadStoragePath: swr.externalInputPayloadStoragePath, + priority: swr.priority, + scheduleStartTime: schedule.scheduleStartTime + ? timestampRendererLocal(schedule.scheduleStartTime) + : "", + scheduleEndTime: schedule.scheduleEndTime + ? timestampRendererLocal(schedule.scheduleEndTime) + : "", + zoneId: schedule.zoneId, + }; + + setScheduleState((prevState) => ({ ...prevState, ...newState })); + setOriginal({ + paused: schedule.paused, + runCatchupScheduleInstances: schedule.runCatchupScheduleInstances, + name: schedule.name, + description: schedule.description, + cronExpression: cronExpression, + scheduleStartTime: schedule.scheduleStartTime + ? schedule.scheduleStartTime + : "", + scheduleEndTime: schedule.scheduleEndTime + ? schedule.scheduleEndTime + : "", + startWorkflowRequest: { + name: swr.name, + version: swr.version ? `${swr.version}` : "", + input: JSON.parse(workflowInput || "{}"), + correlationId: swr.correlationId, + idempotencyKey: swr?.idempotencyKey, + idempotencyStrategy: swr?.idempotencyStrategy, + taskToDomain: JSON.parse(taskToDomainStr || "{}"), + externalInputPayloadStoragePath: swr.externalInputPayloadStoragePath, + priority: swr.priority, + }, + zoneId: schedule.zoneId, + }); + }, + [], + ); + + const initializeFromExecution = useMemo( + () => (latestExecution: any) => { + if (!latestExecution?.workflowName) return; + + const newState = { + workflowVersions: [], // Will be populated by workflow config hook + }; + + setScheduleState((prevState) => ({ ...prevState, ...newState })); + }, + [], + ); + + return { + scheduleState, + setScheduleState, + original, + setOriginal, + initializeFromSchedule, + initializeFromExecution, + }; +} diff --git a/ui-next/src/pages/scheduler/hooks/useWorkflowConfig.ts b/ui-next/src/pages/scheduler/hooks/useWorkflowConfig.ts new file mode 100644 index 0000000000..7c16ed9b8d --- /dev/null +++ b/ui-next/src/pages/scheduler/hooks/useWorkflowConfig.ts @@ -0,0 +1,125 @@ +import _nth from "lodash/nth"; +import { useMemo } from "react"; +import { getTemplateFromInputParams } from "../../runWorkflow/runWorkflowUtils"; +import { IObject } from "types/common"; + +export interface UseWorkflowConfigReturn { + workflowNames: string[]; + workflowVersions: string[]; + workflowInputTemplate: string; + setWorkflowType: (workflowType: string) => { + workflowVersions: string[]; + workflowInputTemplate: string; + }; + setWorkflowVersion: ( + workflowVersion: string | null, + workflowType: string | null, + ) => { + workflowInputTemplate: string; + }; +} + +export function useWorkflowConfig( + workflowDefByVersions: any, + currentWorkflowType: string | null, + currentWorkflowVersions: string[], + currentWorkflowInputTemplate: string, +): UseWorkflowConfigReturn { + const workflowNames = useMemo( + () => + workflowDefByVersions + ? Array.from(workflowDefByVersions.get("lookups").keys()) + : [], + [workflowDefByVersions], + ); + + // Get workflow versions for the current workflow type + const workflowVersions = useMemo(() => { + if (currentWorkflowType && workflowDefByVersions) { + const versions = workflowDefByVersions + .get("lookups") + .get(currentWorkflowType); + return versions ? [...versions] : []; + } + return currentWorkflowVersions; + }, [currentWorkflowType, workflowDefByVersions, currentWorkflowVersions]); + + const setWorkflowType = useMemo( + () => (workflowType: string) => { + let workflowVersionsVal: string[] = []; + let def: IObject = {}; + + if (workflowType !== null) { + workflowVersionsVal = workflowDefByVersions + .get("lookups") + .get(workflowType); + } + + if (workflowVersionsVal && workflowVersionsVal.length > 0) { + const latestVersion = _nth( + workflowVersionsVal, + workflowVersionsVal.length - 1, + ); + if (latestVersion !== null) { + def = workflowDefByVersions + .get("values") + ?.get(workflowType ? workflowType : currentWorkflowType) + ?.get(latestVersion); + } + } + + return { + workflowVersions: [...workflowVersionsVal], + workflowInputTemplate: getTemplateFromInputParams( + def?.["inputParameters"], + ) + ? getTemplateFromInputParams(def?.["inputParameters"]) + : currentWorkflowInputTemplate, + }; + }, + [workflowDefByVersions, currentWorkflowType, currentWorkflowInputTemplate], + ); + + const setWorkflowVersion = useMemo( + () => (workflowVersion: string | null, workflowType: string | null) => { + let def: IObject = {}; + + if (workflowVersion !== null) { + const latestVersion = _nth( + currentWorkflowVersions, + currentWorkflowVersions.length - 1, + ); + const requiredWorkflowVersion = + workflowVersion === "Latest version" + ? latestVersion + : workflowVersion; + def = workflowDefByVersions + .get("values") + ?.get(workflowType ? workflowType : currentWorkflowType) + ?.get(requiredWorkflowVersion); + } + + return { + workflowInputTemplate: getTemplateFromInputParams( + def?.["inputParameters"], + ) + ? getTemplateFromInputParams(def?.["inputParameters"]) + : currentWorkflowInputTemplate, + }; + }, + [ + workflowDefByVersions, + currentWorkflowType, + currentWorkflowVersions, + currentWorkflowInputTemplate, + ], + ); + + return { + workflowNames, + workflowVersions, + workflowInputTemplate: currentWorkflowInputTemplate, + setWorkflowType, + setWorkflowVersion, + }; +} diff --git a/ui-next/src/pages/scheduler/index.ts b/ui-next/src/pages/scheduler/index.ts new file mode 100644 index 0000000000..58c693be47 --- /dev/null +++ b/ui-next/src/pages/scheduler/index.ts @@ -0,0 +1 @@ +export * from "./Schedule"; diff --git a/ui-next/src/pages/scheduler/schedulerHooks.js b/ui-next/src/pages/scheduler/schedulerHooks.js new file mode 100644 index 0000000000..4c335e06be --- /dev/null +++ b/ui-next/src/pages/scheduler/schedulerHooks.js @@ -0,0 +1,29 @@ +import { useAction, useFetch } from "../../utils/query"; +import { useQueryClient } from "react-query"; + +export function useSchedules() { + return useFetch("/scheduler/schedules", { + initialData: [], + }); +} + +export function useSchedule(name) { + return useFetch(`/scheduler/schedules/${name}`, { + enabled: !!name, + }); +} + +export function useSaveSchedule({ onSuccess, ...callbacks }) { + const queryClient = useQueryClient(); + + return useAction("/scheduler/schedules", "post", { + onSuccess: (data, mutationVariables) => { + queryClient.invalidateQueries(); + // TODO properly invalidate only the queries scheduler queries + // queryClient.invalidateQueries(["/scheduler/schedule", mutationVariables.name]); + // queryClient.invalidateQueries("/scheduler/schedules"); + if (onSuccess) onSuccess(data, mutationVariables); + }, + ...callbacks, + }); +} diff --git a/ui-next/src/pages/scheduler/timezones.json b/ui-next/src/pages/scheduler/timezones.json new file mode 100644 index 0000000000..bb990c1f14 --- /dev/null +++ b/ui-next/src/pages/scheduler/timezones.json @@ -0,0 +1,605 @@ +[ + "Africa/Abidjan", + "Africa/Accra", + "Africa/Addis_Ababa", + "Africa/Algiers", + "Africa/Asmara", + "Africa/Asmera", + "Africa/Bamako", + "Africa/Bangui", + "Africa/Banjul", + "Africa/Bissau", + "Africa/Blantyre", + "Africa/Brazzaville", + "Africa/Bujumbura", + "Africa/Cairo", + "Africa/Casablanca", + "Africa/Ceuta", + "Africa/Conakry", + "Africa/Dakar", + "Africa/Dar_es_Salaam", + "Africa/Djibouti", + "Africa/Douala", + "Africa/El_Aaiun", + "Africa/Freetown", + "Africa/Gaborone", + "Africa/Harare", + "Africa/Johannesburg", + "Africa/Juba", + "Africa/Kampala", + "Africa/Khartoum", + "Africa/Kigali", + "Africa/Kinshasa", + "Africa/Lagos", + "Africa/Libreville", + "Africa/Lome", + "Africa/Luanda", + "Africa/Lubumbashi", + "Africa/Lusaka", + "Africa/Malabo", + "Africa/Maputo", + "Africa/Maseru", + "Africa/Mbabane", + "Africa/Mogadishu", + "Africa/Monrovia", + "Africa/Nairobi", + "Africa/Ndjamena", + "Africa/Niamey", + "Africa/Nouakchott", + "Africa/Ouagadougou", + "Africa/Porto-Novo", + "Africa/Sao_Tome", + "Africa/Timbuktu", + "Africa/Tripoli", + "Africa/Tunis", + "Africa/Windhoek", + "America/Adak", + "America/Anchorage", + "America/Anguilla", + "America/Antigua", + "America/Araguaina", + "America/Argentina/Buenos_Aires", + "America/Argentina/Catamarca", + "America/Argentina/ComodRivadavia", + "America/Argentina/Cordoba", + "America/Argentina/Jujuy", + "America/Argentina/La_Rioja", + "America/Argentina/Mendoza", + "America/Argentina/Rio_Gallegos", + "America/Argentina/Salta", + "America/Argentina/San_Juan", + "America/Argentina/San_Luis", + "America/Argentina/Tucuman", + "America/Argentina/Ushuaia", + "America/Aruba", + "America/Asuncion", + "America/Atikokan", + "America/Atka", + "America/Bahia", + "America/Bahia_Banderas", + "America/Barbados", + "America/Belem", + "America/Belize", + "America/Blanc-Sablon", + "America/Boa_Vista", + "America/Bogota", + "America/Boise", + "America/Buenos_Aires", + "America/Cambridge_Bay", + "America/Campo_Grande", + "America/Cancun", + "America/Caracas", + "America/Catamarca", + "America/Cayenne", + "America/Cayman", + "America/Chicago", + "America/Chihuahua", + "America/Ciudad_Juarez", + "America/Coral_Harbour", + "America/Cordoba", + "America/Costa_Rica", + "America/Creston", + "America/Cuiaba", + "America/Curacao", + "America/Danmarkshavn", + "America/Dawson", + "America/Dawson_Creek", + "America/Denver", + "America/Detroit", + "America/Dominica", + "America/Edmonton", + "America/Eirunepe", + "America/El_Salvador", + "America/Ensenada", + "America/Fort_Nelson", + "America/Fort_Wayne", + "America/Fortaleza", + "America/Glace_Bay", + "America/Godthab", + "America/Goose_Bay", + "America/Grand_Turk", + "America/Grenada", + "America/Guadeloupe", + "America/Guatemala", + "America/Guayaquil", + "America/Guyana", + "America/Halifax", + "America/Havana", + "America/Hermosillo", + "America/Indiana/Indianapolis", + "America/Indiana/Knox", + "America/Indiana/Marengo", + "America/Indiana/Petersburg", + "America/Indiana/Tell_City", + "America/Indiana/Vevay", + "America/Indiana/Vincennes", + "America/Indiana/Winamac", + "America/Indianapolis", + "America/Inuvik", + "America/Iqaluit", + "America/Jamaica", + "America/Jujuy", + "America/Juneau", + "America/Kentucky/Louisville", + "America/Kentucky/Monticello", + "America/Knox_IN", + "America/Kralendijk", + "America/La_Paz", + "America/Lima", + "America/Los_Angeles", + "America/Louisville", + "America/Lower_Princes", + "America/Maceio", + "America/Managua", + "America/Manaus", + "America/Marigot", + "America/Martinique", + "America/Matamoros", + "America/Mazatlan", + "America/Mendoza", + "America/Menominee", + "America/Merida", + "America/Metlakatla", + "America/Mexico_City", + "America/Miquelon", + "America/Moncton", + "America/Monterrey", + "America/Montevideo", + "America/Montreal", + "America/Montserrat", + "America/Nassau", + "America/New_York", + "America/Nipigon", + "America/Nome", + "America/Noronha", + "America/North_Dakota/Beulah", + "America/North_Dakota/Center", + "America/North_Dakota/New_Salem", + "America/Nuuk", + "America/Ojinaga", + "America/Panama", + "America/Pangnirtung", + "America/Paramaribo", + "America/Phoenix", + "America/Port-au-Prince", + "America/Port_of_Spain", + "America/Porto_Acre", + "America/Porto_Velho", + "America/Puerto_Rico", + "America/Punta_Arenas", + "America/Rainy_River", + "America/Rankin_Inlet", + "America/Recife", + "America/Regina", + "America/Resolute", + "America/Rio_Branco", + "America/Rosario", + "America/Santa_Isabel", + "America/Santarem", + "America/Santiago", + "America/Santo_Domingo", + "America/Sao_Paulo", + "America/Scoresbysund", + "America/Shiprock", + "America/Sitka", + "America/St_Barthelemy", + "America/St_Johns", + "America/St_Kitts", + "America/St_Lucia", + "America/St_Thomas", + "America/St_Vincent", + "America/Swift_Current", + "America/Tegucigalpa", + "America/Thule", + "America/Thunder_Bay", + "America/Tijuana", + "America/Toronto", + "America/Tortola", + "America/Vancouver", + "America/Virgin", + "America/Whitehorse", + "America/Winnipeg", + "America/Yakutat", + "America/Yellowknife", + "Antarctica/Casey", + "Antarctica/Davis", + "Antarctica/DumontDUrville", + "Antarctica/Macquarie", + "Antarctica/Mawson", + "Antarctica/McMurdo", + "Antarctica/Palmer", + "Antarctica/Rothera", + "Antarctica/South_Pole", + "Antarctica/Syowa", + "Antarctica/Troll", + "Antarctica/Vostok", + "Arctic/Longyearbyen", + "Asia/Aden", + "Asia/Almaty", + "Asia/Amman", + "Asia/Anadyr", + "Asia/Aqtau", + "Asia/Aqtobe", + "Asia/Ashgabat", + "Asia/Ashkhabad", + "Asia/Atyrau", + "Asia/Baghdad", + "Asia/Bahrain", + "Asia/Baku", + "Asia/Bangkok", + "Asia/Barnaul", + "Asia/Beirut", + "Asia/Bishkek", + "Asia/Brunei", + "Asia/Calcutta", + "Asia/Chita", + "Asia/Choibalsan", + "Asia/Chongqing", + "Asia/Chungking", + "Asia/Colombo", + "Asia/Dacca", + "Asia/Damascus", + "Asia/Dhaka", + "Asia/Dili", + "Asia/Dubai", + "Asia/Dushanbe", + "Asia/Famagusta", + "Asia/Gaza", + "Asia/Harbin", + "Asia/Hebron", + "Asia/Ho_Chi_Minh", + "Asia/Hong_Kong", + "Asia/Hovd", + "Asia/Irkutsk", + "Asia/Istanbul", + "Asia/Jakarta", + "Asia/Jayapura", + "Asia/Jerusalem", + "Asia/Kabul", + "Asia/Kamchatka", + "Asia/Karachi", + "Asia/Kashgar", + "Asia/Kathmandu", + "Asia/Katmandu", + "Asia/Khandyga", + "Asia/Kolkata", + "Asia/Krasnoyarsk", + "Asia/Kuala_Lumpur", + "Asia/Kuching", + "Asia/Kuwait", + "Asia/Macao", + "Asia/Macau", + "Asia/Magadan", + "Asia/Makassar", + "Asia/Manila", + "Asia/Muscat", + "Asia/Nicosia", + "Asia/Novokuznetsk", + "Asia/Novosibirsk", + "Asia/Omsk", + "Asia/Oral", + "Asia/Phnom_Penh", + "Asia/Pontianak", + "Asia/Pyongyang", + "Asia/Qatar", + "Asia/Qostanay", + "Asia/Qyzylorda", + "Asia/Rangoon", + "Asia/Riyadh", + "Asia/Saigon", + "Asia/Sakhalin", + "Asia/Samarkand", + "Asia/Seoul", + "Asia/Shanghai", + "Asia/Singapore", + "Asia/Srednekolymsk", + "Asia/Taipei", + "Asia/Tashkent", + "Asia/Tbilisi", + "Asia/Tehran", + "Asia/Tel_Aviv", + "Asia/Thimbu", + "Asia/Thimphu", + "Asia/Tokyo", + "Asia/Tomsk", + "Asia/Ujung_Pandang", + "Asia/Ulaanbaatar", + "Asia/Ulan_Bator", + "Asia/Urumqi", + "Asia/Ust-Nera", + "Asia/Vientiane", + "Asia/Vladivostok", + "Asia/Yakutsk", + "Asia/Yangon", + "Asia/Yekaterinburg", + "Asia/Yerevan", + "Atlantic/Azores", + "Atlantic/Bermuda", + "Atlantic/Canary", + "Atlantic/Cape_Verde", + "Atlantic/Faeroe", + "Atlantic/Faroe", + "Atlantic/Jan_Mayen", + "Atlantic/Madeira", + "Atlantic/Reykjavik", + "Atlantic/South_Georgia", + "Atlantic/St_Helena", + "Atlantic/Stanley", + "Australia/ACT", + "Australia/Adelaide", + "Australia/Brisbane", + "Australia/Broken_Hill", + "Australia/Canberra", + "Australia/Currie", + "Australia/Darwin", + "Australia/Eucla", + "Australia/Hobart", + "Australia/LHI", + "Australia/Lindeman", + "Australia/Lord_Howe", + "Australia/Melbourne", + "Australia/NSW", + "Australia/North", + "Australia/Perth", + "Australia/Queensland", + "Australia/South", + "Australia/Sydney", + "Australia/Tasmania", + "Australia/Victoria", + "Australia/West", + "Australia/Yancowinna", + "Brazil/Acre", + "Brazil/DeNoronha", + "Brazil/East", + "Brazil/West", + "CET", + "CST6CDT", + "Canada/Atlantic", + "Canada/Central", + "Canada/Eastern", + "Canada/Mountain", + "Canada/Newfoundland", + "Canada/Pacific", + "Canada/Saskatchewan", + "Canada/Yukon", + "Chile/Continental", + "Chile/EasterIsland", + "Cuba", + "EET", + "EST5EDT", + "Egypt", + "Eire", + "Etc/GMT", + "Etc/GMT+0", + "Etc/GMT+1", + "Etc/GMT+10", + "Etc/GMT+11", + "Etc/GMT+12", + "Etc/GMT+2", + "Etc/GMT+3", + "Etc/GMT+4", + "Etc/GMT+5", + "Etc/GMT+6", + "Etc/GMT+7", + "Etc/GMT+8", + "Etc/GMT+9", + "Etc/GMT-0", + "Etc/GMT-1", + "Etc/GMT-10", + "Etc/GMT-11", + "Etc/GMT-12", + "Etc/GMT-13", + "Etc/GMT-14", + "Etc/GMT-2", + "Etc/GMT-3", + "Etc/GMT-4", + "Etc/GMT-5", + "Etc/GMT-6", + "Etc/GMT-7", + "Etc/GMT-8", + "Etc/GMT-9", + "Etc/GMT0", + "Etc/Greenwich", + "Etc/UCT", + "Etc/UTC", + "Etc/Universal", + "Etc/Zulu", + "Europe/Amsterdam", + "Europe/Andorra", + "Europe/Astrakhan", + "Europe/Athens", + "Europe/Belfast", + "Europe/Belgrade", + "Europe/Berlin", + "Europe/Bratislava", + "Europe/Brussels", + "Europe/Bucharest", + "Europe/Budapest", + "Europe/Busingen", + "Europe/Chisinau", + "Europe/Copenhagen", + "Europe/Dublin", + "Europe/Gibraltar", + "Europe/Guernsey", + "Europe/Helsinki", + "Europe/Isle_of_Man", + "Europe/Istanbul", + "Europe/Jersey", + "Europe/Kaliningrad", + "Europe/Kiev", + "Europe/Kirov", + "Europe/Kyiv", + "Europe/Lisbon", + "Europe/Ljubljana", + "Europe/London", + "Europe/Luxembourg", + "Europe/Madrid", + "Europe/Malta", + "Europe/Mariehamn", + "Europe/Minsk", + "Europe/Monaco", + "Europe/Moscow", + "Europe/Nicosia", + "Europe/Oslo", + "Europe/Paris", + "Europe/Podgorica", + "Europe/Prague", + "Europe/Riga", + "Europe/Rome", + "Europe/Samara", + "Europe/San_Marino", + "Europe/Sarajevo", + "Europe/Saratov", + "Europe/Simferopol", + "Europe/Skopje", + "Europe/Sofia", + "Europe/Stockholm", + "Europe/Tallinn", + "Europe/Tirane", + "Europe/Tiraspol", + "Europe/Ulyanovsk", + "Europe/Uzhgorod", + "Europe/Vaduz", + "Europe/Vatican", + "Europe/Vienna", + "Europe/Vilnius", + "Europe/Volgograd", + "Europe/Warsaw", + "Europe/Zagreb", + "Europe/Zaporozhye", + "Europe/Zurich", + "GB", + "GB-Eire", + "GMT", + "GMT0", + "Greenwich", + "Hongkong", + "Iceland", + "Indian/Antananarivo", + "Indian/Chagos", + "Indian/Christmas", + "Indian/Cocos", + "Indian/Comoro", + "Indian/Kerguelen", + "Indian/Mahe", + "Indian/Maldives", + "Indian/Mauritius", + "Indian/Mayotte", + "Indian/Reunion", + "Iran", + "Israel", + "Jamaica", + "Japan", + "Kwajalein", + "Libya", + "MET", + "MST7MDT", + "Mexico/BajaNorte", + "Mexico/BajaSur", + "Mexico/General", + "NZ", + "NZ-CHAT", + "Navajo", + "PRC", + "PST8PDT", + "Pacific/Apia", + "Pacific/Auckland", + "Pacific/Bougainville", + "Pacific/Chatham", + "Pacific/Chuuk", + "Pacific/Easter", + "Pacific/Efate", + "Pacific/Enderbury", + "Pacific/Fakaofo", + "Pacific/Fiji", + "Pacific/Funafuti", + "Pacific/Galapagos", + "Pacific/Gambier", + "Pacific/Guadalcanal", + "Pacific/Guam", + "Pacific/Honolulu", + "Pacific/Johnston", + "Pacific/Kanton", + "Pacific/Kiritimati", + "Pacific/Kosrae", + "Pacific/Kwajalein", + "Pacific/Majuro", + "Pacific/Marquesas", + "Pacific/Midway", + "Pacific/Nauru", + "Pacific/Niue", + "Pacific/Norfolk", + "Pacific/Noumea", + "Pacific/Pago_Pago", + "Pacific/Palau", + "Pacific/Pitcairn", + "Pacific/Pohnpei", + "Pacific/Ponape", + "Pacific/Port_Moresby", + "Pacific/Rarotonga", + "Pacific/Saipan", + "Pacific/Samoa", + "Pacific/Tahiti", + "Pacific/Tarawa", + "Pacific/Tongatapu", + "Pacific/Truk", + "Pacific/Wake", + "Pacific/Wallis", + "Pacific/Yap", + "Poland", + "Portugal", + "ROK", + "Singapore", + "SystemV/AST4", + "SystemV/AST4ADT", + "SystemV/CST6", + "SystemV/CST6CDT", + "SystemV/EST5", + "SystemV/EST5EDT", + "SystemV/HST10", + "SystemV/MST7", + "SystemV/MST7MDT", + "SystemV/PST8", + "SystemV/PST8PDT", + "SystemV/YST9", + "SystemV/YST9YDT", + "Turkey", + "UCT", + "US/Alaska", + "US/Aleutian", + "US/Arizona", + "US/Central", + "US/East-Indiana", + "US/Eastern", + "US/Hawaii", + "US/Indiana-Starke", + "US/Michigan", + "US/Mountain", + "US/Pacific", + "US/Samoa", + "UTC", + "Universal", + "W-SU", + "WET", + "Zulu" +] diff --git a/ui-next/src/pages/scheduler/utils/scheduleTransformers.ts b/ui-next/src/pages/scheduler/utils/scheduleTransformers.ts new file mode 100644 index 0000000000..53e62aabba --- /dev/null +++ b/ui-next/src/pages/scheduler/utils/scheduleTransformers.ts @@ -0,0 +1,128 @@ +import _get from "lodash/get"; +import { timestampRendererLocal } from "utils/index"; +import { tryToJson } from "utils/index"; +import { WorkflowDef } from "types/WorkflowDef"; +import { ScheduleType } from "../Schedule"; + +/** + * Parse JSON string safely, returning null for empty strings + */ +export function JSONParse(text: string) { + if (text) { + return JSON.parse(text); + } + return null; +} + +/** + * Convert date field to timestamp value + */ +export function getDateFromField(d1: string | number | Date) { + if (d1) { + return new Date(d1).valueOf(); + } + return ""; +} + +/** + * Convert form data to code representation + */ +export function formToCodeData( + scheduleState: ScheduleType, + schedule: any, +): Partial | null { + const start = getDateFromField(scheduleState.scheduleStartTime); + const to = getDateFromField(scheduleState.scheduleEndTime); + + let input; + try { + input = JSONParse(scheduleState.workflowInputTemplate); + } catch { + return null; + } + + let taskToDomain; + try { + taskToDomain = JSONParse(scheduleState.taskToDomain); + } catch { + return null; + } + + const body = { + id: _get(schedule, "id"), + paused: scheduleState.paused, + runCatchupScheduleInstances: scheduleState.runCatchupScheduleInstances, + name: scheduleState.name, + description: scheduleState.description, + cronExpression: scheduleState.cronExpression, + scheduleStartTime: start, + scheduleEndTime: to, + startWorkflowRequest: { + name: scheduleState.workflowType, + version: scheduleState.workflowVersion, + input: input ? input : {}, + correlationId: scheduleState.workflowCorrelationId, + idempotencyKey: scheduleState?.workflowIdempotencyKey, + idempotencyStrategy: scheduleState?.workflowIdempotencyStrategy, + taskToDomain: taskToDomain ? taskToDomain : {}, + workflowDef: tryToJson(scheduleState.workflowDef), + externalInputPayloadStoragePath: + scheduleState.externalInputPayloadStoragePath, + priority: scheduleState.priority, + }, + zoneId: scheduleState.zoneId, + }; + + return body; +} + +/** + * Convert code data to form representation + */ +export function codeToFormData( + data: string, + scheduleState: ScheduleType, +): ScheduleType { + const changedData = tryToJson(data); + const body = { + name: changedData?.name || "", + description: changedData?.description || "", + cronExpression: changedData?.cronExpression || "", + runCatchupScheduleInstances: !!changedData?.runCatchupScheduleInstances, + paused: !!changedData?.paused, + workflowType: changedData?.startWorkflowRequest?.name, + workflowVersions: scheduleState.workflowVersions, + workflowVersion: changedData?.startWorkflowRequest?.version, + workflowCorrelationId: changedData?.startWorkflowRequest?.correlationId, + workflowIdempotencyKey: changedData?.startWorkflowRequest?.idempotencyKey, + workflowIdempotencyStrategy: + changedData?.startWorkflowRequest?.idempotencyStrategy, + workflowInputTemplate: JSON.stringify( + changedData?.startWorkflowRequest?.input, + null, + 2, + ), + taskToDomain: JSON.stringify( + changedData?.startWorkflowRequest?.taskToDomain, + null, + 2, + ), + workflowDef: JSON.stringify( + changedData?.startWorkflowRequest?.workflowDef, + null, + 2, + ), + externalInputPayloadStoragePath: + changedData?.startWorkflowRequest?.externalInputPayloadStoragePath, + priority: changedData?.startWorkflowRequest?.priority, + scheduleStartTime: changedData?.scheduleStartTime + ? timestampRendererLocal(changedData?.scheduleStartTime) + : "", + scheduleEndTime: changedData?.scheduleEndTime + ? timestampRendererLocal(changedData?.scheduleEndTime) + : "", + zoneId: changedData?.zoneId, + }; + + return body; +} diff --git a/ui-next/src/pages/styles.js b/ui-next/src/pages/styles.js new file mode 100644 index 0000000000..aba4f964aa --- /dev/null +++ b/ui-next/src/pages/styles.js @@ -0,0 +1,173 @@ +import { colors } from "theme/tokens/variables"; + +export default { + wrapper: { + overflowY: "auto", + overflowX: "hidden", + height: "100%", + width: "100%", + display: "contents", + justifyContent: "flexStart", + backgroundColor: colors.gray14, + }, + fullWidth: { + width: "100%", + height: "100%", + overflowY: "scroll", + backgroundColor: "white", + paddingBottom: "400px", + }, + padded: { + padding: "20px", + }, + header: { + backgroundColor: colors.gray14, + paddingLeft: "50px", + paddingTop: "20px", + "@media (min-width: 1920px)": { + paddingLeft: "50px", + }, + }, + tabContent: { + marginTop: "10px", + paddingTop: "20px", + paddingRight: "20px", + paddingBottom: "50px", + paddingLeft: "20px", + "@media (min-width: 1920px)": { + paddingLeft: "50px", + }, + }, + gridFlex: { + display: "flex", + margin: 0, + padding: 0, + overflow: "auto", + width: "100%", + flexWrap: "nowrap", + alignItems: "stretch", + justifyContent: "space-between", + minWidth: "900px", + }, + fixedDisplayHeader: { + backgroundColor: colors.gray14, + paddingLeft: "50px", + paddingTop: "20px", + "@media (min-width: 1920px)": { + paddingLeft: "50px", + }, + overflowY: "scroll", + overflowX: "hidden", + display: "block", + justifyContent: "flexStart", + position: "sticky", + left: 0, + top: 0, + zIndex: 10, + }, + tabContentScroll: { + paddingTop: 0, + paddingRight: "20px", + paddingBottom: "50px", + paddingLeft: "20px", + "@media (min-width: 1920px)": { + paddingLeft: "50px", + }, + overflowY: "scroll", + overflowX: "hidden", + }, + paperMargin: { + marginBottom: "30px", + }, + iconButton: { + color: "black", + opacity: 0.3, + paddingRight: "10px", + fontSize: 18, + "&:hover": { + opacity: 0.8, + backgroundColor: "transparent", + }, + }, + editorLabel: { + color: "black", + opacity: 0.8, + paddingLeft: "10px", + fontSize: "12px", + lineHeight: 3, + fontWeight: "400", + "& span": { + fontSize: "13px", + fontWeight: "bold", + }, + "& svg": { + fontSize: "18px", + }, + }, + chipContainer: { + display: "flex", + flexWrap: "wrap", + "& > *": { + margin: "2px 5px 2px 0", + }, + }, + resizer: { + width: "10px", + margin: "-5px", + cursor: "col-resize", + backgroundColor: "rgb(45, 45, 45, 0.05)", + zIndex: 1, + flexShrink: 0, + resize: "horizontal", + "&:hover": { + backgroundColor: "rgb(45, 45, 45, 0.3)", + }, + }, + workflowDefFirstRowMenu: { + width: "100%", + display: "flex", + flexFlow: "row", + justifyContent: "space-around", + paddingTop: 3, + paddingBottom: 3, + alignItems: "center", + // position: "sticky", + // top: 0, + // left: 0, + }, + definitionEditorSecondRowMenu: { + width: "100%", + display: "flex", + flexFlow: "row", + justifyContent: "space-around", + paddingTop: "3px", + paddingBottom: "6px", + borderBottom: "solid var(--backgroundLight) 2px", + alignItems: "center", + position: "sticky", + top: 0, + left: 0, + }, + popover: { + "& .MuiPopover-paper": { + padding: "8px", + backgroundColor: "rgb(45, 45, 45, 0.6)", + color: "white", + }, + "& .MuiTypography-root": { + fontSize: "14px", + }, + }, + deleteIcon: { + "& svg": { + color: "#cd5c5c", + fontSize: "14px", + }, + }, + switch: { + "& .MuiSwitch-thumb": { + position: "relative", + top: "-2px", + }, + }, +}; diff --git a/ui-next/src/pages/tags/TagsDashboard.tsx b/ui-next/src/pages/tags/TagsDashboard.tsx new file mode 100644 index 0000000000..5ad14f56de --- /dev/null +++ b/ui-next/src/pages/tags/TagsDashboard.tsx @@ -0,0 +1,364 @@ +import { Box, Paper, Typography, Chip } from "@mui/material"; +import { ArrowClockwise as RefreshIcon } from "@phosphor-icons/react"; +import { DataTable } from "components"; +import Button from "components/MuiButton"; +import { useCallback, useMemo, useState } from "react"; +import { Helmet } from "react-helmet"; +import SectionContainer from "shared/SectionContainer"; +import SectionHeader from "shared/SectionHeader"; +import Header from "components/Header"; +import { useBatchedTagsData } from "utils/hooks"; +import NoDataComponent from "components/NoDataComponent"; +import { colors } from "theme/tokens/variables"; + +interface TagAggregation { + tag: string; + workflows: number; + tasks: number; + webhooks: number; + events: number; + templates: number; + schedules: number; + secrets: number; + prompts: number; + environments: number; + integrations: number; + total: number; +} + +export default function TagsDashboard() { + const [isRefreshing, setIsRefreshing] = useState(false); + + // Use the batched API hook instead of multiple individual calls + const { + data: batchedData, + refetch: refetchBatchedData, + isLoading: isFetching, + } = useBatchedTagsData(); + + // Resource type to entity type mapping + const resourceTypeMap: Record< + string, + keyof Omit + > = { + WORKFLOW_DEF: "workflows", + TASK_DEF: "tasks", + WEBHOOK: "webhooks", + EVENT_HANDLER: "events", + USER_FORM_TEMPLATE: "templates", + WORKFLOW_SCHEDULE: "schedules", + SECRET_NAME: "secrets", + PROMPT: "prompts", + ENV_VARIABLE: "environments", + INTEGRATION_PROVIDER: "integrations", + }; + + const handleRefresh = useCallback(async () => { + setIsRefreshing(true); + try { + await refetchBatchedData(); + } finally { + setIsRefreshing(false); + } + }, [refetchBatchedData]); + + // Process batched data to create tag aggregations + const tagAggregations = useMemo((): TagAggregation[] => { + // Create a map to track tag usage across different entity types + const tagMap = new Map(); + + // Process the batched data array + if (Array.isArray(batchedData)) { + batchedData?.forEach((item) => { + const { tagKey, tagValue, resourceType, countPerResourceType } = item; + const tagkeyValue = `${tagKey}:${tagValue}`; + + // Initialize the aggregation if it doesn't exist + if (!tagMap.has(tagkeyValue)) { + tagMap.set(tagkeyValue, { + tag: tagkeyValue, + workflows: 0, + tasks: 0, + webhooks: 0, + events: 0, + templates: 0, + schedules: 0, + secrets: 0, + prompts: 0, + environments: 0, + integrations: 0, + total: 0, + }); + } + + const aggregation = tagMap.get(tagkeyValue)!; + const entityType = resourceTypeMap[resourceType]; + + // If the resource type is mapped, add the count + if (entityType) { + aggregation[entityType] += countPerResourceType || 0; + aggregation.total += countPerResourceType || 0; + } + }); + } + + // Convert map to array and sort by total count (descending), then by tag name (ascending) + return Array.from(tagMap.values()).sort((a, b) => { + if (b.total !== a.total) { + return b.total - a.total; + } + return a.tag.localeCompare(b.tag); + }); + }, [batchedData]); + + console.log("tagAggregations", tagAggregations); + + const columns = useMemo( + () => [ + { + id: "tag", + name: "tag", + label: "Tag", + grow: 3, + renderer: (tag: string) => ( + + ), + tooltip: "Tag name", + }, + { + id: "workflows", + name: "workflows", + label: "Workflows", + renderer: (count: number) => ( + + {count} + + ), + tooltip: "Number of workflows with this tag", + }, + { + id: "tasks", + name: "tasks", + label: "Tasks", + renderer: (count: number) => ( + + {count} + + ), + tooltip: "Number of task definitions with this tag", + }, + { + id: "webhooks", + name: "webhooks", + label: "Webhooks", + renderer: (count: number) => ( + + {count} + + ), + tooltip: "Number of webhooks with this tag", + }, + { + id: "events", + name: "events", + label: "Events", + renderer: (count: number) => ( + + {count} + + ), + tooltip: "Number of event handlers with this tag", + }, + { + id: "templates", + name: "templates", + label: "Templates", + renderer: (count: number) => ( + + {count} + + ), + tooltip: "Number of templates with this tag", + }, + { + id: "schedules", + name: "schedules", + label: "Schedules", + renderer: (count: number) => ( + + {count} + + ), + tooltip: "Number of schedules with this tag", + }, + { + id: "secrets", + name: "secrets", + label: "Secrets", + renderer: (count: number) => ( + + {count} + + ), + tooltip: "Number of secrets with this tag", + }, + { + id: "prompts", + name: "prompts", + label: "Prompts", + renderer: (count: number) => ( + + {count} + + ), + tooltip: "Number of prompts with this tag", + }, + { + id: "environments", + name: "environments", + label: "Environments", + renderer: (count: number) => ( + + {count} + + ), + tooltip: "Number of environment variables with this tag", + }, + { + id: "integrations", + name: "integrations", + label: "Integrations", + renderer: (count: number) => ( + + {count} + + ), + tooltip: "Number of integrations with this tag", + }, + { + id: "total", + name: "total", + label: "Total", + renderer: (count: number) => ( + + {count} + + ), + tooltip: "Total number of definitions with this tag", + }, + ], + [], + ); + + return ( + <> + + Tags Dashboard + + + + + +
    + } + /> + + + +
    + {isFetching ? ( + + Loading tags data... + + + ) : ( + + } + /> + )} + + + + ); +} diff --git a/ui-next/src/pages/tags/TagsList.jsx b/ui-next/src/pages/tags/TagsList.jsx new file mode 100644 index 0000000000..f96c4bd048 --- /dev/null +++ b/ui-next/src/pages/tags/TagsList.jsx @@ -0,0 +1,144 @@ +import { Box, Tooltip } from "@mui/material"; +import { + Trash as DeleteIcon, + ArrowClockwise as RefreshIcon, +} from "@phosphor-icons/react"; +import { DataTable, NavLink, Paper } from "components"; +import ConfirmChoiceDialog from "components/enterprise/ConfirmChoiceDialog"; +import Button from "components/MuiButton"; +import IconButton from "components/MuiIconButton"; +import sharedStyles from "pages/styles"; +import { useState } from "react"; +import { Helmet } from "react-helmet"; +import SectionContainer from "shared/SectionContainer"; +import SectionHeader from "shared/SectionHeader"; +import { featureFlags, FEATURES } from "utils/flags"; +import { usePushHistory } from "utils/hooks/usePushHistory"; +import { useActionWithPath, useFetch } from "utils/query"; + +export default function TaskDefinitions() { + const columns = [ + { + id: "key", + name: "key", + label: "Tag Key", + renderer: (key) => {key}, + }, + { id: "value", name: "value", label: "Value", grow: 2 }, + { + id: "actions", + name: "name", + label: "Actions", + sortable: false, + searchable: false, + grow: 0.5, + renderer: (name) => ( + + + { + setIsConfirmDelete({ + confirmDelete: true, + name: name, + }); + }} + label="Delete" + sx={sharedStyles.deleteIcon} + > + + + + + ), + }, + ]; + + const [isConfirmDelete, setIsConfirmDelete] = useState(false); + + const tagVisibility = featureFlags.getValue(FEATURES.TAG_VISIBILITY, "READ"); + + const pushHistory = usePushHistory(); + const { data, refetch } = useFetch(`/metadata/tags?access=${tagVisibility}`, { + initialData: [{ type: "METADATA", key: "team", value: "devops" }], + }); + + const deleteTagAction = useActionWithPath({ + onSuccess: () => { + refetch(); + }, + onError: (err) => { + console.error(err); + refetch(); + }, + }); + + return ( + <> + + Tags + + {isConfirmDelete && ( + { + if (selectedChoice) { + deleteTagAction.mutate({ + method: "delete", + path: `/metadata/tags/${isConfirmDelete.name}`, + }); + } + setIsConfirmDelete(false); + }} + message={ + "Are you sure you want to delete this Tag? This cannot be undone." + } + /> + )} + + + + } + /> + + + {/*
    */} + {data && ( + <> + + + , + ]} + /> + + )} + + + + ); +} diff --git a/ui-next/src/plugins/AppBarModules.jsx b/ui-next/src/plugins/AppBarModules.jsx new file mode 100644 index 0000000000..cd233e794e --- /dev/null +++ b/ui-next/src/plugins/AppBarModules.jsx @@ -0,0 +1,54 @@ +import { Box, Tooltip } from "@mui/material"; +import { List } from "@phosphor-icons/react"; + +import { useAuth } from "shared/auth"; +import { FEATURES, featureFlags } from "utils"; + +import { IconButton, NavLink } from "components"; +import AppLogo from "plugins/AppLogo"; + +export default function AppBarModules({ handleDrawBarOpen }) { + const { user } = useAuth(); + + if (!featureFlags.isEnabled(FEATURES.ACCESS_MANAGEMENT)) { + return null; + } + + return user ? ( + <> + + + + + + + + + + + + + ) : null; +} diff --git a/ui-next/src/plugins/AppLogo.jsx b/ui-next/src/plugins/AppLogo.jsx new file mode 100644 index 0000000000..1f464d4a96 --- /dev/null +++ b/ui-next/src/plugins/AppLogo.jsx @@ -0,0 +1,31 @@ +import { OpenedLogo } from "components/Sidebar/OpenedLogo"; +import { useContext, useMemo } from "react"; +import { ColorModeContext } from "theme/material/ColorModeContext"; +import { FEATURES, featureFlags } from "utils"; + +const customLogo = featureFlags.getValue(FEATURES.CUSTOM_LOGO_URL); + +export default function AppLogo() { + const { mode } = useContext(ColorModeContext); + + const imgSrc = useMemo(() => { + if (mode === "light") { + return "/orkes-logo-purple-2x.png"; + } + + return "/orkes-logo-purple-inverted-2x.png"; + }, [mode]); + + return customLogo ? ( + + ) : ( + Orkes Conductor + ); +} diff --git a/ui-next/src/plugins/ConductorLogo.jsx b/ui-next/src/plugins/ConductorLogo.jsx new file mode 100644 index 0000000000..d3a50c8a29 --- /dev/null +++ b/ui-next/src/plugins/ConductorLogo.jsx @@ -0,0 +1,12 @@ +export default function ConductorLogo() { + return ( + Conductor + ); +} diff --git a/ui-next/src/plugins/constants.js b/ui-next/src/plugins/constants.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ui-next/src/plugins/customTypeRenderers.jsx b/ui-next/src/plugins/customTypeRenderers.jsx new file mode 100644 index 0000000000..d645915ae7 --- /dev/null +++ b/ui-next/src/plugins/customTypeRenderers.jsx @@ -0,0 +1 @@ +export const customTypeRenderers = {}; diff --git a/ui-next/src/plugins/env.js b/ui-next/src/plugins/env.js new file mode 100644 index 0000000000..505ef3e7c9 --- /dev/null +++ b/ui-next/src/plugins/env.js @@ -0,0 +1,6 @@ +export function useEnv() { + return { + stack: "default", + defaultStack: "default", + }; +} diff --git a/ui-next/src/plugins/fetch.ts b/ui-next/src/plugins/fetch.ts new file mode 100644 index 0000000000..1a0ff3b562 --- /dev/null +++ b/ui-next/src/plugins/fetch.ts @@ -0,0 +1,74 @@ +/** + * Fetch utilities for OSS mode. + * + * This simplified version removes auth token handling since + * OSS mode does not use authentication. + */ +import { MessageContext } from "components/v1/layout/MessageContext"; +import { useContext } from "react"; +import { IObject } from "types/common"; +import { getErrorMessage, tryToJson } from "utils/utils"; +import { useEnv as hardcodeEnv } from "./env"; + +const { VITE_ENVIRONMENT, VITE_WF_SERVER } = process.env; + +export function fetchContextNonHook() { + const { stack } = hardcodeEnv(); + + return { + stack, + ready: true, + }; +} + +export function useFetchContext() { + const contextNonHook = fetchContextNonHook(); + const { setMessage } = useContext(MessageContext); + + return { + ...contextNonHook, + setMessage, + }; +} + +export async function fetchWithContext( + path: string, + context: IObject, + fetchParams: IObject, + isText?: boolean, + throwOnError = true, +): Promise { + const newParams = { ...fetchParams }; + + // Need for build version (can't use proxy) + const newPath = `${ + VITE_ENVIRONMENT === "test" ? VITE_WF_SERVER : "" + }/api/${path}`; + + const cleanPath = newPath.replace(/([^:]\/)\/+/g, "$1"); // Cleanup duplicated slashes + + const res = await fetch(cleanPath, newParams); + + // Handle error cases + if (!res.ok) { + const hasContext = context && context?.setMessage != null; + // 1. Using global message + if (hasContext && !throwOnError) { + const errorMessage = await getErrorMessage(res); + context.setMessage({ text: errorMessage, severity: "error" }); + + return null; + } + + // 2. Throw the error to handle locally + throw res; + } + + const text = await res.text(); + + if (!text || text.length === 0) { + return null; + } + + return isText ? text : tryToJson(text); +} diff --git a/ui-next/src/plugins/index.ts b/ui-next/src/plugins/index.ts new file mode 100644 index 0000000000..8c4e94397d --- /dev/null +++ b/ui-next/src/plugins/index.ts @@ -0,0 +1,40 @@ +/** + * Plugins Module + * + * This module provides the plugin system and extension points for Conductor UI. + * + * - Plugin Registry: Register plugins to extend routes, sidebar, task forms, etc. + * - Fetch: Authenticated HTTP client + * - Custom Type Renderers: Extension point for custom task type rendering + */ + +// Plugin registry - the main extension system +export { + pluginRegistry, + registerPlugin, + // Types + type ConductorPlugin, + type PluginRegistry, + type PluginTaskFormProps, + type TaskFormRegistration, + type TaskMenuCategory, + type TaskMenuItemRegistration, + type SidebarMenuTarget, + type SidebarItemPosition, + type SidebarItemRegistration, + type AuthProviderProps, + type AuthProviderRegistration, + type SearchResultItem, + type SearchDataFetcher, + type SearchResultMapper, + type SearchProviderRegistration, + type SidebarExtension, + type TaskDocUrlRegistration, +} from "./registry"; + +// Fetch utilities +export { + fetchWithContext, + useFetchContext, + fetchContextNonHook, +} from "./fetch"; diff --git a/ui-next/src/plugins/registry/index.ts b/ui-next/src/plugins/registry/index.ts new file mode 100644 index 0000000000..e8011cbed5 --- /dev/null +++ b/ui-next/src/plugins/registry/index.ts @@ -0,0 +1,62 @@ +/** + * Plugin Registry + * + * This module provides the plugin system for Conductor UI. + * Use registerPlugin() to add plugins that extend the application. + * + * @example + * ```typescript + * import { registerPlugin, ConductorPlugin } from 'plugins/registry'; + * + * const myPlugin: ConductorPlugin = { + * id: 'my-plugin', + * name: 'My Plugin', + * routes: [...], + * sidebarItems: [...], + * taskForms: [...], + * }; + * + * registerPlugin(myPlugin); + * ``` + */ + +// Export the registry singleton and convenience function +export { pluginRegistry, registerPlugin } from "./registry"; + +// Export all types +export type { + // Main plugin interface + ConductorPlugin, + PluginRegistry, + // Task form types + PluginTaskFormProps, + TaskFormRegistration, + // Task menu types + TaskMenuCategory, + TaskMenuItemRegistration, + // Sidebar types + SidebarMenuTarget, + SidebarItemPosition, + SidebarItemRegistration, + // Auth provider types + AuthProviderProps, + AuthProviderRegistration, + // Search provider types + SearchResultItem, + SearchDataFetcher, + SearchResultMapper, + SearchProviderRegistration, + // Sidebar extension types + SidebarExtension, + // Task doc URL types + TaskDocUrlRegistration, + // Dependency section types + DependencySectionRegistration, + DependencySectionProps, + WorkflowDependencies, + // Schema dialog types + SchemaEditDialogProps, + SchemaPreviewDialogProps, + // Generated key dialog types + GeneratedKeyDialogProps, +} from "./types"; diff --git a/ui-next/src/plugins/registry/registry.ts b/ui-next/src/plugins/registry/registry.ts new file mode 100644 index 0000000000..a7206fbb5d --- /dev/null +++ b/ui-next/src/plugins/registry/registry.ts @@ -0,0 +1,397 @@ +/** + * Plugin Registry Implementation + * + * Singleton registry that manages all registered plugins and provides + * methods to access their contributed functionality. + */ + +import { ComponentType, ReactNode } from "react"; +import { RouteObject } from "react-router-dom"; +import { + AuthProviderProps, + ConductorPlugin, + DependencySectionRegistration, + GeneratedKeyDialogProps, + NewIntegrationModalProps, + PluginRegistry, + PluginTaskFormProps, + SchemaEditDialogProps, + SchemaPreviewDialogProps, + SearchProviderRegistration, + SidebarExtension, + SidebarItemRegistration, + TaskMenuItemRegistration, +} from "./types"; + +/** + * Creates a new plugin registry instance + */ +function createPluginRegistry(): PluginRegistry { + // Storage for registered plugins + const plugins: ConductorPlugin[] = []; + + // Cached lookups for performance + const taskFormCache = new Map>(); + const authProviderCache = new Map>(); + const taskDocUrlCache = new Map(); + + // Flag to track if caches need rebuilding + let cachesDirty = true; + + /** + * Rebuild all caches from registered plugins + */ + function rebuildCaches(): void { + if (!cachesDirty) return; + + taskFormCache.clear(); + authProviderCache.clear(); + taskDocUrlCache.clear(); + + for (const plugin of plugins) { + // Cache task forms + if (plugin.taskForms) { + for (const registration of plugin.taskForms) { + taskFormCache.set(registration.taskType, registration.component); + } + } + + // Cache auth providers + if (plugin.authProviders) { + for (const registration of plugin.authProviders) { + authProviderCache.set(registration.type, registration.component); + } + } + + // Cache task doc URLs + if (plugin.taskDocUrls) { + for (const registration of plugin.taskDocUrls) { + taskDocUrlCache.set(registration.taskType, registration.url); + } + } + } + + cachesDirty = false; + } + + return { + /** + * Register a plugin with the registry + */ + register(plugin: ConductorPlugin): void { + // Check for duplicate plugin IDs + const existing = plugins.find((p) => p.id === plugin.id); + if (existing) { + console.warn( + `Plugin with ID "${plugin.id}" is already registered. Skipping.`, + ); + return; + } + + plugins.push(plugin); + cachesDirty = true; + + // Call plugin's onRegister hook if provided + if (plugin.onRegister) { + try { + plugin.onRegister(); + } catch (error) { + console.error( + `Error in onRegister hook for plugin "${plugin.id}":`, + error, + ); + } + } + + console.log(`Plugin registered: ${plugin.name} (${plugin.id})`); + }, + + /** + * Get all registered plugins + */ + getPlugins(): ConductorPlugin[] { + return [...plugins]; + }, + + /** + * Get all authenticated routes from plugins + */ + getRoutes(): RouteObject[] { + const routes: RouteObject[] = []; + for (const plugin of plugins) { + if (plugin.routes) { + routes.push(...plugin.routes); + } + } + return routes; + }, + + /** + * Get all public routes from plugins + */ + getPublicRoutes(): RouteObject[] { + const routes: RouteObject[] = []; + for (const plugin of plugins) { + if (plugin.publicRoutes) { + routes.push(...plugin.publicRoutes); + } + } + return routes; + }, + + /** + * Get all sidebar items from plugins, sorted by position + */ + getSidebarItems(): SidebarItemRegistration[] { + const items: SidebarItemRegistration[] = []; + for (const plugin of plugins) { + if (plugin.sidebarItems) { + items.push(...plugin.sidebarItems); + } + } + return items; + }, + + /** + * Get a task form component for a given task type + */ + getTaskForm(taskType: string): ComponentType | null { + rebuildCaches(); + return taskFormCache.get(taskType) || null; + }, + + /** + * Get all task menu items from plugins + */ + getTaskMenuItems(): TaskMenuItemRegistration[] { + const items: TaskMenuItemRegistration[] = []; + for (const plugin of plugins) { + if (plugin.taskMenuItems) { + // Filter out hidden items + const visibleItems = plugin.taskMenuItems.filter( + (item) => !item.hidden, + ); + items.push(...visibleItems); + } + } + return items; + }, + + /** + * Get an auth provider component for a given type + */ + getAuthProvider(type: string): ComponentType | null { + rebuildCaches(); + return authProviderCache.get(type) || null; + }, + + /** + * Get all search providers from plugins, sorted by priority + */ + getSearchProviders(): SearchProviderRegistration[] { + const providers: SearchProviderRegistration[] = []; + for (const plugin of plugins) { + if (plugin.searchProviders) { + providers.push(...plugin.searchProviders); + } + } + // Sort by priority (lower = higher priority) + return providers.sort( + (a, b) => (a.priority ?? 100) - (b.priority ?? 100), + ); + }, + + /** + * Get all sidebar extensions from plugins + */ + getSidebarExtensions(): SidebarExtension[] { + const extensions: SidebarExtension[] = []; + for (const plugin of plugins) { + if (plugin.sidebarExtensions) { + extensions.push(...plugin.sidebarExtensions); + } + } + return extensions; + }, + + /** + * Get a task documentation URL for a given task type + */ + getTaskDocUrl(taskType: string): string | null { + rebuildCaches(); + return taskDocUrlCache.get(taskType) || null; + }, + + /** + * Get all task documentation URLs from plugins as a Record + */ + getTaskDocUrls(): Record { + rebuildCaches(); + const urls: Record = {}; + taskDocUrlCache.forEach((url, type) => { + urls[type] = url; + }); + return urls; + }, + + /** + * Get all global components from plugins + */ + getGlobalComponents(): ComponentType[] { + const components: ComponentType[] = []; + for (const plugin of plugins) { + if (plugin.globalComponents) { + components.push(...plugin.globalComponents); + } + } + return components; + }, + + /** + * Get the "Create New Integration" modal component. + * Returns the first registered one, or null if none (OSS build). + */ + getNewIntegrationModal(): ComponentType | null { + for (const plugin of plugins) { + if (plugin.newIntegrationModal) { + return plugin.newIntegrationModal; + } + } + return null; + }, + + /** + * Get the playground home page component. + * Returns the first registered one, or null if none. + */ + getPlaygroundHomePage(): ComponentType | null { + for (const plugin of plugins) { + if (plugin.playgroundHomePage) { + return plugin.playgroundHomePage; + } + } + return null; + }, + + /** + * Get the app layout component. + * Returns the first registered one, or null (use BaseLayout as fallback). + */ + getAppLayout(): ComponentType<{ children: ReactNode }> | null { + for (const plugin of plugins) { + if (plugin.appLayout) { + return plugin.appLayout; + } + } + return null; + }, + + /** + * Get all dependency sections for the workflow editor's Dependencies tab. + * Returns sections sorted by order. + */ + getDependencySections(): DependencySectionRegistration[] { + const sections: DependencySectionRegistration[] = []; + for (const plugin of plugins) { + if (plugin.dependencySections) { + sections.push(...plugin.dependencySections); + } + } + // Sort by order (lower = first) + return sections.sort((a, b) => a.order - b.order); + }, + + /** + * Get the schema edit dialog component. + * Returns the first registered one, or null if none (OSS build). + */ + getSchemaEditDialog(): ComponentType | null { + for (const plugin of plugins) { + if (plugin.schemaEditDialog) { + return plugin.schemaEditDialog; + } + } + return null; + }, + + /** + * Get the schema preview dialog component. + * Returns the first registered one, or null if none (OSS build). + */ + getSchemaPreviewDialog(): ComponentType | null { + for (const plugin of plugins) { + if (plugin.schemaPreviewDialog) { + return plugin.schemaPreviewDialog; + } + } + return null; + }, + + /** + * Get the generated key dialog component. + * Returns the first registered one, or null if none (OSS build). + */ + getGeneratedKeyDialog(): ComponentType | null { + for (const plugin of plugins) { + if (plugin.generatedKeyDialog) { + return plugin.generatedKeyDialog; + } + } + return null; + }, + + /** + * Get the login page component. + * Returns the first registered one, or null if none (OSS build). + */ + getLoginPage(): ComponentType | null { + for (const plugin of plugins) { + if (plugin.loginPage) { + return plugin.loginPage; + } + } + return null; + }, + + /** + * Get the auth guard component. + * Returns the first registered one, or null if none (OSS build). + */ + getAuthGuard(): ComponentType<{ + fallback?: ReactNode; + runWorkflow?: boolean; + }> | null { + for (const plugin of plugins) { + if (plugin.authGuard) { + return plugin.authGuard; + } + } + return null; + }, + + /** + * Get the access token for API requests. + * Returns null in OSS builds (no authentication). + */ + getAccessToken(): string | null { + for (const plugin of plugins) { + if (plugin.getAccessToken) { + return plugin.getAccessToken(); + } + } + return null; + }, + }; +} + +/** + * The global plugin registry singleton + */ +export const pluginRegistry = createPluginRegistry(); + +/** + * Convenience function to register a plugin + */ +export function registerPlugin(plugin: ConductorPlugin): void { + pluginRegistry.register(plugin); +} diff --git a/ui-next/src/plugins/registry/types.ts b/ui-next/src/plugins/registry/types.ts new file mode 100644 index 0000000000..b4793add78 --- /dev/null +++ b/ui-next/src/plugins/registry/types.ts @@ -0,0 +1,651 @@ +/** + * Plugin Registry Types + * + * This module defines the interfaces for the Conductor UI plugin system. + * Plugins can extend the application with: + * - Routes (authenticated and public) + * - Sidebar menu items + * - Task forms for the workflow editor + * - Task menu items for the "Add Task" menu + * - Authentication providers + * - Search providers for global search + * - Sidebar state machine extensions + * - Task documentation URLs + */ + +import { ComponentType, ReactNode } from "react"; +import { RouteObject } from "react-router-dom"; +import { CSSObject } from "@mui/material/styles"; +import { AuthHeaders } from "types/common"; +import { BaseIntegration, IntegrationDef } from "types/Integrations"; + +// ============================================================================ +// Task Form Types +// ============================================================================ + +/** + * Props passed to task form components in the workflow editor + */ +export interface PluginTaskFormProps { + task: Record; + onChange: (task: Record) => void; + updateAdditionalFieldMetadata?: any; + additionalFieldMetadata?: any; + isMetaBarEditing?: boolean; + onToggleExpand?: (workflowName: string) => void; + collapseWorkflowList?: string[]; + taskFormHeaderActor?: any; // ActorRef from xstate +} + +/** + * Registration for a task form component + */ +export interface TaskFormRegistration { + /** The task type (e.g., "HUMAN", "WAIT_FOR_WEBHOOK") */ + taskType: string; + /** The React component to render for this task type */ + component: ComponentType; +} + +// ============================================================================ +// Task Menu Types +// ============================================================================ + +/** + * Category tabs in the "Add Task" menu + */ +export type TaskMenuCategory = + | "ALL" + | "System" + | "Operators" + | "Alerting" + | "Workers" + | "AI Tasks" + | "Integrations"; + +/** + * Registration for a task type in the "Add Task" menu + */ +export interface TaskMenuItemRegistration { + /** Display name in the menu */ + name: string; + /** Description shown below the name */ + description: string; + /** The task type constant (e.g., "HUMAN", "WAIT_FOR_WEBHOOK") */ + type: string; + /** Category tab where this task appears */ + category: TaskMenuCategory; + /** Optional version number */ + version?: number; + /** If true, this item is hidden (can be used with feature flags) */ + hidden?: boolean; + /** + * If true, this task type appears in the QuickAdd grid of the workflow editor. + * Enterprise plugins use this to surface their task types in the quick-add panel. + */ + quickAdd?: boolean; +} + +// ============================================================================ +// Sidebar Types +// ============================================================================ + +/** + * Target submenu where a sidebar item should be inserted + */ +export type SidebarMenuTarget = + | "executionsSubMenu" + | "definitionsSubMenu" + | "apiSubMenu" + | "adminSubMenu" + | "helpMenu" + | "root"; // For top-level items + +/** + * Position hint for where to insert the item within the target menu + */ +export type SidebarItemPosition = "start" | "end" | number; + +/** + * Registration for a sidebar menu item + */ +export interface SidebarItemRegistration { + /** Unique identifier for this menu item */ + id: string; + /** Display title */ + title: string; + /** Icon element (can be null for submenu items) */ + icon: ReactNode; + /** Route path to navigate to (empty string for submenu parents) */ + linkTo: string; + /** Additional routes that should mark this item as active */ + activeRoutes?: string[]; + /** Keyboard shortcuts */ + shortcuts?: string[]; + /** Hotkey string */ + hotkeys?: string; + /** Target submenu to insert into */ + targetMenu: SidebarMenuTarget; + /** Position within the target menu */ + position?: SidebarItemPosition; + /** Whether this item is hidden */ + hidden?: boolean; + /** Open link in new tab */ + isOpenNewTab?: boolean; + /** Custom text styles */ + textStyle?: CSSObject; + /** Custom button container styles */ + buttonContainerStyle?: CSSObject; + /** Custom icon container styles */ + iconContainerStyles?: CSSObject; + /** Click handler (instead of navigation) */ + handler?: () => void; + /** Custom component to render instead of default */ + component?: ReactNode; + /** Nested submenu items (for creating new submenus) */ + items?: SidebarItemRegistration[]; + /** + * Optional React hook that returns the current badge count for this item. + * When the returned value is > 0, a red badge with the count is shown next + * to the item title. Enterprise plugins use this to show pending task counts. + * + * Must follow React hook rules (called unconditionally in component render). + */ + useBadgeCount?: () => number; +} + +// ============================================================================ +// Auth Provider Types +// ============================================================================ + +/** + * Props for auth provider wrapper components + */ +export interface AuthProviderProps { + children: ReactNode; +} + +/** + * Registration for an authentication provider + */ +export interface AuthProviderRegistration { + /** Provider type identifier (e.g., "auth0", "okta", "oidc") */ + type: string; + /** The provider component that wraps the app */ + component: ComponentType; +} + +// ============================================================================ +// Search Provider Types +// ============================================================================ + +/** + * A search result item returned by search providers + */ +export interface SearchResultItem { + /** Icon to display */ + icon?: ReactNode; + /** Display title */ + title: string; + /** Route to navigate to when clicked */ + route?: string; + /** Nested results (for grouped results) */ + sub?: SearchResultItem[]; +} + +/** + * Function type for fetching search data + */ +export type SearchDataFetcher = (authHeaders?: AuthHeaders) => Promise; + +/** + * Function type for transforming fetched data into search results + */ +export type SearchResultMapper = ( + data: any[], + searchTerm: string, +) => SearchResultItem[]; + +/** + * Registration for a search provider + */ +export interface SearchProviderRegistration { + /** Unique identifier for this search provider */ + id: string; + /** Human-readable name for the search category */ + name: string; + /** Function to fetch the searchable data */ + fetcher: SearchDataFetcher; + /** Function to map data to search results */ + mapper: SearchResultMapper; + /** Priority for ordering results (lower = higher priority) */ + priority?: number; +} + +// ============================================================================ +// Sidebar Extension Types +// ============================================================================ + +/** + * Extension for the sidebar state machine (e.g., for polling human tasks) + */ +export interface SidebarExtension { + /** Unique identifier */ + id: string; + /** + * Initial context values to merge into sidebar machine context + */ + initialContext?: Record; + /** + * Service function to invoke (e.g., for polling) + * Returns data that will be passed to the onDone handler + */ + service?: (context: any) => Promise; + /** + * Interval in milliseconds for polling (if applicable) + */ + pollingInterval?: number; + /** + * Action to run when service completes + */ + onServiceDone?: (context: any, data: any) => Record; +} + +// ============================================================================ +// Task Doc URL Types +// ============================================================================ + +/** + * Registration for task documentation URLs + */ +export interface TaskDocUrlRegistration { + /** The task type */ + taskType: string; + /** The documentation URL */ + url: string; +} + +// ============================================================================ +// New Integration Modal Types +// ============================================================================ + +/** + * Props for the "Create New Integration" modal component registered by + * the enterprise integrations plugin. + */ +export interface NewIntegrationModalProps { + integrationDefList: IntegrationDef[]; + integrationToEdit: Partial; + onClose: () => void; + onAfterSave?: (savedIntegration: BaseIntegration) => void; + nameEditable?: boolean; + isNewIntegration?: boolean; +} + +// ============================================================================ +// Schema Dialog Types (for SchemaForm in workflow editor) +// ============================================================================ + +/** + * Props for the SchemaEditDialog component. + */ +export interface SchemaEditDialogProps { + initialData: { + schemaName: string; + schemaVersion?: string; + isNewSchema: boolean; + }; + open?: boolean; + onClose?: (schema?: { name: string; version?: number }) => void; +} + +/** + * Props for the SchemaPreviewDialog component. + */ +export interface SchemaPreviewDialogProps { + schemaName: string; + schemaVersion?: number; + open?: boolean; + onClose?: () => void; +} + +// ============================================================================ +// Generated Key Dialog Types (for WorkflowPropertiesForm) +// ============================================================================ + +/** + * Props for the GeneratedKeyDialog component used in MetadataBanner. + */ +export interface GeneratedKeyDialogProps { + handleClose: () => void; + applicationAccessKey: { id: string; secret: string }; + setIsToastOpen: (open: boolean) => void; +} + +// ============================================================================ +// Dependency Section Types (for DependenciesTab in workflow editor) +// ============================================================================ + +/** + * Dependencies extracted from a workflow definition. + * This matches the return type of scanTasksForDependenciesInWorkflow. + */ +export interface WorkflowDependencies { + integrationNames: string[]; + promptNames: string[]; + userFormsNameVersion: Array<{ name: string; version?: string }>; + schemas: Array<{ name: string; version?: string }>; + secrets: string[]; + env: string[]; + workflowName?: string; + workflowVersion?: number; +} + +/** + * Props passed to dependency section components in the workflow editor's Dependencies tab. + */ +export interface DependencySectionProps { + /** All extracted workflow dependencies */ + dependencies: WorkflowDependencies; +} + +/** + * Registration for a dependency section in the workflow editor's Dependencies tab. + */ +export interface DependencySectionRegistration { + /** Unique identifier for this section */ + id: string; + /** Title displayed in the collapsible section header */ + title: string; + /** Order in which sections appear (lower = first) */ + order: number; + /** React component that renders the section content */ + component: ComponentType; +} + +// ============================================================================ +// Main Plugin Interface +// ============================================================================ + +/** + * A Conductor UI plugin that can extend the application + */ +export interface ConductorPlugin { + /** + * Unique identifier for the plugin + */ + id: string; + + /** + * Human-readable name + */ + name: string; + + /** + * Plugin version + */ + version?: string; + + /** + * Routes to add inside the AuthGuard (authenticated routes) + */ + routes?: RouteObject[]; + + /** + * Routes to add outside the AuthGuard (public routes like login callbacks) + */ + publicRoutes?: RouteObject[]; + + /** + * Sidebar menu items to add + */ + sidebarItems?: SidebarItemRegistration[]; + + /** + * Task form components to register + */ + taskForms?: TaskFormRegistration[]; + + /** + * Task menu items for the "Add Task" menu + */ + taskMenuItems?: TaskMenuItemRegistration[]; + + /** + * Authentication providers + */ + authProviders?: AuthProviderRegistration[]; + + /** + * Search providers for global search + */ + searchProviders?: SearchProviderRegistration[]; + + /** + * Sidebar state machine extensions + */ + sidebarExtensions?: SidebarExtension[]; + + /** + * Task documentation URLs + */ + taskDocUrls?: TaskDocUrlRegistration[]; + + /** + * Global React components to mount inside the authenticated app layout. + * Use this for invisible side-effect components such as pollers. + * Components receive no props and must be self-contained. + */ + globalComponents?: ComponentType[]; + + /** + * A component that renders the "Create New Integration" modal used in the + * RichAddTaskMenu Integrations tab. The component receives the base + * integration template and callbacks via props injected by AddTaskSidebar. + * Enterprise plugins use this to supply the IntegrationEditModel. + */ + newIntegrationModal?: ComponentType; + + /** + * The page component rendered at the root path "/" in playground mode. + * Enterprise plugins (e.g. the hub/playground plugin) register this to + * show the template showcase. When not registered, the normal app is shown. + */ + playgroundHomePage?: ComponentType; + + /** + * Replacement layout component for the main app shell (sidebar + top bar). + * Enterprise plugins register an agent-aware layout here; OSS uses BaseLayout. + * Must accept `{ children: ReactNode }`. + */ + appLayout?: ComponentType<{ children: ReactNode }>; + + /** + * Sections to add to the Dependencies tab in the workflow editor. + * Enterprise plugins register sections for integrations, prompts, secrets, etc. + */ + dependencySections?: DependencySectionRegistration[]; + + /** + * Schema edit dialog component for inline schema editing in task forms. + * Enterprise plugins register this; OSS builds hide edit buttons when null. + */ + schemaEditDialog?: ComponentType; + + /** + * Schema preview dialog component for previewing schemas in task forms. + * Enterprise plugins register this; OSS builds hide preview buttons when null. + */ + schemaPreviewDialog?: ComponentType; + + /** + * Generated key dialog component for displaying access keys in MetadataBanner. + * Enterprise plugins register this; OSS builds use a simple fallback. + */ + generatedKeyDialog?: ComponentType; + + /** + * Login page component rendered when user is not authenticated. + * Enterprise plugins register this; OSS builds show a simple fallback message. + */ + loginPage?: ComponentType; + + /** + * Auth guard component that wraps authenticated routes. + * Enterprise plugins register this to enforce authentication. + * OSS builds use a simple layout wrapper with no auth checks. + * Must render for child routes. + */ + authGuard?: ComponentType<{ fallback?: ReactNode; runWorkflow?: boolean }>; + + /** + * Function to get the current access token for API requests. + * Enterprise plugins register this to provide JWT tokens from their auth provider. + * OSS builds return null (no authentication). + */ + getAccessToken?: () => string | null; + + /** + * Initialization function called when plugin is registered + */ + onRegister?: () => void; +} + +// ============================================================================ +// Registry Interface +// ============================================================================ + +/** + * The plugin registry interface + */ +export interface PluginRegistry { + /** + * Register a plugin + */ + register(plugin: ConductorPlugin): void; + + /** + * Get all registered plugins + */ + getPlugins(): ConductorPlugin[]; + + /** + * Get all authenticated routes from plugins + */ + getRoutes(): RouteObject[]; + + /** + * Get all public routes from plugins + */ + getPublicRoutes(): RouteObject[]; + + /** + * Get all sidebar items from plugins + */ + getSidebarItems(): SidebarItemRegistration[]; + + /** + * Get a task form component for a given task type + */ + getTaskForm(taskType: string): ComponentType | null; + + /** + * Get all task menu items from plugins + */ + getTaskMenuItems(): TaskMenuItemRegistration[]; + + /** + * Get an auth provider component for a given type + */ + getAuthProvider(type: string): ComponentType | null; + + /** + * Get all search providers from plugins + */ + getSearchProviders(): SearchProviderRegistration[]; + + /** + * Get all sidebar extensions from plugins + */ + getSidebarExtensions(): SidebarExtension[]; + + /** + * Get a task documentation URL for a given task type + */ + getTaskDocUrl(taskType: string): string | null; + + /** + * Get all task documentation URLs from plugins + */ + getTaskDocUrls(): Record; + + /** + * Get all global components from plugins + */ + getGlobalComponents(): ComponentType[]; + + /** + * Get the "Create New Integration" modal component from the integrations plugin. + * Returns null in OSS builds (no integrations plugin registered). + */ + getNewIntegrationModal(): ComponentType | null; + + /** + * Get the playground home page component. + * Returns null when not registered (OSS or non-playground builds). + */ + getPlaygroundHomePage(): ComponentType | null; + + /** + * Get the app layout component (sidebar + top bar shell). + * Returns null when not registered; callers should fall back to BaseLayout. + */ + getAppLayout(): ComponentType<{ children: ReactNode }> | null; + + /** + * Get all dependency sections for the workflow editor's Dependencies tab. + * Returns sections sorted by order. + */ + getDependencySections(): DependencySectionRegistration[]; + + /** + * Get the schema edit dialog component. + * Returns null in OSS builds (no schema plugin registered). + */ + getSchemaEditDialog(): ComponentType | null; + + /** + * Get the schema preview dialog component. + * Returns null in OSS builds (no schema plugin registered). + */ + getSchemaPreviewDialog(): ComponentType | null; + + /** + * Get the generated key dialog component. + * Returns null in OSS builds (no access plugin registered). + */ + getGeneratedKeyDialog(): ComponentType | null; + + /** + * Get the login page component. + * Returns null in OSS builds (no auth plugin registered). + */ + getLoginPage(): ComponentType | null; + + /** + * Get the auth guard component. + * Returns null in OSS builds (no auth plugin registered). + * When null, routes.tsx uses the default OSS AuthGuard (layout wrapper only). + */ + getAuthGuard(): ComponentType<{ + fallback?: ReactNode; + runWorkflow?: boolean; + }> | null; + + /** + * Get the access token for API requests. + * Returns null in OSS builds (no authentication). + * Enterprise plugins provide the JWT token from their auth provider. + */ + getAccessToken(): string | null; +} diff --git a/ui-next/src/queryClient.ts b/ui-next/src/queryClient.ts new file mode 100644 index 0000000000..c1c75da816 --- /dev/null +++ b/ui-next/src/queryClient.ts @@ -0,0 +1,10 @@ +import { QueryClient } from "react-query"; + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + cacheTime: 600000, // 10 mins + }, + }, +}); diff --git a/ui-next/src/routes/__tests__/router.test.tsx b/ui-next/src/routes/__tests__/router.test.tsx new file mode 100644 index 0000000000..f76b4ee084 --- /dev/null +++ b/ui-next/src/routes/__tests__/router.test.tsx @@ -0,0 +1,788 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +// Mock HTMLCanvasElement.getContext for tests +// This needs to be set up before any modules that use canvas are imported +Object.defineProperty(HTMLCanvasElement.prototype, "getContext", { + value: vi.fn(function (this: HTMLCanvasElement) { + // Ensure this canvas has backingStorePixelRatio + if (!("backingStorePixelRatio" in this)) { + Object.defineProperty(this, "backingStorePixelRatio", { + get: () => 1, + configurable: true, + }); + } + + const context = { + fillRect: vi.fn(), + clearRect: vi.fn(), + getImageData: vi.fn(() => ({ data: new Array(4) })), + putImageData: vi.fn(), + createImageData: vi.fn(() => []), + setTransform: vi.fn(), + drawImage: vi.fn(), + save: vi.fn(), + fillText: vi.fn(), + restore: vi.fn(), + beginPath: vi.fn(), + moveTo: vi.fn(), + lineTo: vi.fn(), + closePath: vi.fn(), + stroke: vi.fn(), + translate: vi.fn(), + scale: vi.fn(), + rotate: vi.fn(), + arc: vi.fn(), + fill: vi.fn(), + measureText: vi.fn(() => ({ width: 0 })), + transform: vi.fn(), + rect: vi.fn(), + clip: vi.fn(), + canvas: this, // Reference to the canvas element - ensure it's not null + }; + return context; + }), + configurable: true, +}); + +// Mock canvas element properties that might be accessed +// backingStorePixelRatio is a deprecated property but still used by some libraries +Object.defineProperty(HTMLCanvasElement.prototype, "backingStorePixelRatio", { + get: function () { + return 1; + }, + configurable: true, +}); + +// Ensure width and height properties exist +Object.defineProperty(HTMLCanvasElement.prototype, "width", { + get: function () { + return parseInt(this.getAttribute("width") || "0", 10) || 0; + }, + set: function (value) { + this.setAttribute("width", String(value)); + }, + configurable: true, +}); + +Object.defineProperty(HTMLCanvasElement.prototype, "height", { + get: function () { + return parseInt(this.getAttribute("height") || "0", 10) || 0; + }, + set: function (value) { + this.setAttribute("height", String(value)); + }, + configurable: true, +}); + +// Mock window.devicePixelRatio if not already set +if (typeof window !== "undefined" && !window.devicePixelRatio) { + Object.defineProperty(window, "devicePixelRatio", { + get: () => 1, + configurable: true, + }); +} + +// Also ensure that when a canvas is created, it has these properties +const originalCreateElement = document.createElement.bind(document); +document.createElement = function (tagName: string, options?: any) { + const element = originalCreateElement(tagName, options); + if (tagName.toLowerCase() === "canvas") { + // Ensure the canvas has backingStorePixelRatio + if (!("backingStorePixelRatio" in element)) { + Object.defineProperty(element, "backingStorePixelRatio", { + get: () => 1, + configurable: true, + }); + } + } + return element; +}; + +// Mock @okta/okta-signin-widget to prevent canvas issues +vi.mock("@okta/okta-signin-widget", () => ({ + default: vi.fn(), +})); + +// Mock all dependencies first - use factory function to avoid hoisting issues +vi.mock("utils", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + featureFlags: { + isEnabled: vi.fn(() => false), + getValue: vi.fn(), + getContextValue: vi.fn(), + }, + FEATURES: { + PLAYGROUND: "PLAYGROUND", + SHOW_GET_STARTED_PAGE: "SHOW_GET_STARTED_PAGE", + TASK_INDEXING: "TASK_INDEXING", + }, + }; +}); + +// Mock route constants (must include all used by conductor-ui routes.tsx) +vi.mock("utils/constants/route", () => ({ + API_REFERENCE_URL: { BASE: "/api-reference" }, + AI_PROMPTS_MANAGEMENT_URL: { BASE: "/ai_prompts" }, + APPLICATION_MANAGEMENT_URL: { BASE: "/applications" }, + AUTHENTICATION_URL: "/authentication", + ENV_VARIABLES_URL: { BASE: "/env-variables" }, + EVENT_HANDLERS_URL: { + BASE: "/eventHandlers", + NAME: "/eventHandlers/:name", + NEW: "/eventHandlers/new", + }, + EVENT_MONITOR_URL: { BASE: "/event-monitor", NAME: "/event-monitor/:name" }, + GROUP_MANAGEMENT_URL: { BASE: "/groups" }, + ROLE_MANAGEMENT_URL: { + BASE: "/roleManagement", + TYPE_ID: "/roleManagement/:type?/:id?", + LIST: "/roleManagement/roles", + EDIT: "/roleManagement/roles/:id", + }, + HUMAN_TASK_URL: { + TASK_ID: "/human/:taskId", + LIST: "/human", + TEMPLATES: "/human/templates", + TEMPLATES_NAME_VERSION: "/human/templates/:name/:version", + TASK_INBOX: "/human/inbox", + }, + INTEGRATIONS_MANAGEMENT_URL: { BASE: "/integrations" }, + NEW_TASK_DEF_URL: "/taskDef/new", + REMOTE_SERVICES_URL: { + BASE: "/remote-services", + NEW: "/remote-services/new", + EDIT: "/remote-services/:id/edit", + }, + RUN_WORKFLOW_URL: "/runWorkflow", + SCHEDULER_DEFINITION_URL: { + BASE: "/scheduleDef", + NAME: "/scheduleDef/:name", + NEW: "/scheduleDef/new", + }, + SCHEMAS_URL: { BASE: "/schemas", EDIT: "/schemas/:id/edit" }, + SECRETS_URL: { BASE: "/secrets" }, + SERVICE_URL: { + LIST: "/services", + SERVICE_ID: "/services/:serviceId", + NEW: "/services/new", + EDIT: "/services/:serviceId/edit", + NEW_ROUTE: "/services/:serviceId/routes/new", + ROUTE_DETAILS: "/services/:serviceId/routes/:routeId", + ROUTE_EDIT: "/services/:serviceId/routes/:routeId/edit", + }, + TASK_DEF_URL: { BASE: "/taskDef", NAME: "/taskDef/:name" }, + TASK_EXECUTION_URL: { LIST: "/taskExecution" }, + TASK_QUEUE_URL: { BASE: "/taskQueue" }, + USER_MANAGEMENT_URL: { BASE: "/users" }, + WEBHOOK_ROUTE_URL: { + NEW: "/webhook/new", + ID: "/webhook/:id", + LIST: "/webhooks", + }, + WORKFLOW_DEFINITION_URL: { + BASE: "/workflowDef", + NAME_VERSION: "/workflowDef/:name/:version", + NEW: "/workflowDef/new", + }, + WORKFLOW_EXPLORER_URL: "/workflow-explorer", + WORKERS_URL: { + BASE: "/workers", + }, + TAGS_DASHBOARD_URL: { BASE: "/tags-dashboard" }, +})); + +// Mock all page components with factory functions +vi.mock("@okta/okta-react", () => ({ + LoginCallback: () => ({ type: "LoginCallback" }), +})); +vi.mock("components/auth/AuthGuard", () => ({ + default: () => ({ type: "AuthGuard" }), +})); +vi.mock("components/App", () => ({ App: () => ({ type: "App" }) })); +vi.mock("enterprise/pages/access/ApplicationManagement", () => ({ + default: () => ({ type: "ApplicationManagement" }), +})); +vi.mock("enterprise/pages/access/GroupManagement", () => ({ + default: () => ({ type: "GroupManagement" }), +})); +vi.mock("enterprise/pages/access/users/UserManagement", () => ({ + default: () => ({ type: "UserManagement" }), +})); +vi.mock("enterprise/pages/aiPrompts/AiPromptsManagement", () => ({ + default: () => ({ type: "AiPromptsManagement" }), +})); +vi.mock("enterprise/pages/Authentication/AuthListing", () => ({ + default: () => ({ type: "AuthListing" }), +})); +vi.mock("pages/creatorFlags/CreatorFlags", () => ({ + CreatorFlags: () => ({ type: "CreatorFlags" }), +})); +vi.mock("pages/definition/task", () => ({ + TaskDefinition: () => ({ type: "TaskDefinition" }), +})); +vi.mock("pages/definition/WorkflowDefinition", () => ({ + default: () => ({ type: "WorkflowDefinition" }), +})); +vi.mock("pages/definitions", () => ({ + EventHandler: () => ({ type: "EventHandler" }), + Schedules: () => ({ type: "Schedules" }), + Task: () => ({ type: "Task" }), + Workflow: () => ({ type: "Workflow" }), +})); +vi.mock("enterprise/pages/envVariables/EnvVariables", () => ({ + EnvVariables: () => ({ type: "EnvVariables" }), +})); +vi.mock("pages/error/ErrorPage", () => ({ + default: () => ({ type: "ErrorPage" }), +})); +vi.mock("pages/eventMonitor/EventMonitor", () => ({ + EventMonitor: () => ({ type: "EventMonitor" }), +})); +vi.mock("pages/eventMonitor/EventMonitorDetail/EventMonitorDetail", () => ({ + EventMonitorDetail: () => ({ type: "EventMonitorDetail" }), +})); +vi.mock("pages/executions", () => ({ + SchedulerExecutions: () => ({ type: "SchedulerExecutions" }), + TaskSearch: () => ({ type: "TaskSearch" }), + WorkflowSearch: () => ({ type: "WorkflowSearch" }), +})); +vi.mock("enterprise/pages/getStarted/GetStarted", () => ({ + default: () => ({ type: "GetStarted" }), +})); +vi.mock("enterprise/pages/hub/hub", () => ({ + HubMain: () => ({ type: "HubMain" }), + HubPage: () => ({ type: "HubPage" }), + HubTemplateDetail: () => ({ type: "HubTemplateDetail" }), + HubTemplateImport: () => ({ type: "HubTemplateImport" }), +})); +vi.mock("enterprise/pages/human", () => ({ + SearchPage: () => ({ type: "SearchPage" }), +})); +vi.mock("enterprise/pages/human/humanTask", () => ({ + TaskPage: () => ({ type: "TaskPage" }), +})); +vi.mock("enterprise/pages/human/search/TaskInboxPage", () => ({ + TaskInboxPage: () => ({ type: "TaskInboxPage" }), +})); +vi.mock("enterprise/pages/human/templates", () => ({ + TemplateEditorPage: () => ({ type: "TemplateEditorPage" }), + TemplatePage: () => ({ type: "TemplatePage" }), +})); +vi.mock("enterprise/pages/integrations/IntegrationsManagement", () => ({ + default: () => ({ type: "IntegrationsManagement" }), +})); +vi.mock("enterprise/pages/metrics", () => ({ + default: () => ({ type: "MetricsPage" }), +})); +vi.mock("enterprise/pages/remoteServices/edit/ServiceEdit", () => ({ + default: () => ({ type: "ServiceEdit" }), +})); +vi.mock("enterprise/pages/remoteServices/Services", () => ({ + Services: () => ({ type: "Services" }), +})); +vi.mock("enterprise/pages/schema/edit/SchemaEditPage", () => ({ + SchemaEditPage: () => ({ type: "SchemaEditPage" }), +})); +vi.mock("enterprise/pages/schema/list/SchemaList", () => ({ + SchemaList: () => ({ type: "SchemaList" }), +})); +vi.mock("enterprise/pages/secrets/Secrets", () => ({ + default: () => ({ type: "Secrets" }), +})); +vi.mock("enterprise/pages/services/EditService", () => ({ + default: () => ({ type: "EditService" }), +})); +vi.mock("enterprise/pages/services/NewService", () => ({ + default: () => ({ type: "NewService" }), +})); +vi.mock("enterprise/pages/services/routes/EditRoute", () => ({ + default: () => ({ type: "EditRoute" }), +})); +vi.mock("enterprise/pages/services/routes/NewRoute", () => ({ + default: () => ({ type: "NewRoute" }), +})); +vi.mock("enterprise/pages/services/routes/RouteDetails", () => ({ + default: () => ({ type: "RouteDetails" }), +})); +vi.mock("enterprise/pages/services/Service", () => ({ + default: () => ({ type: "Service" }), +})); +vi.mock("enterprise/pages/services/Services", () => ({ + default: () => ({ type: "Services" }), +})); +vi.mock("enterprise/pages/webhooks", () => ({ + Webhooks: () => ({ type: "Webhooks" }), +})); +vi.mock("enterprise/pages/webhooks/edit/WebhookEdit", () => ({ + WebhookEditPage: () => ({ type: "WebhookEditPage" }), +})); +vi.mock("enterprise/pages/workflowExplorer/Explorer", () => ({ + default: () => ({ type: "Explorer" }), +})); +vi.mock("shared/auth/oidc/OidcRedirectEndpoint", () => ({ + OidcRedirectEndpoint: () => ({ type: "OidcRedirectEndpoint" }), +})); +vi.mock("enterprise/pages/auth/Login", () => ({ + default: () => ({ type: "Login" }), +})); +vi.mock("../pages/definition/EventHandler/EventHandler", () => ({ + default: () => ({ type: "EventHandlerDefinition" }), +})); +vi.mock("../pages/execution/Execution", () => ({ + default: () => ({ type: "Execution" }), +})); +vi.mock("../pages/kitchensink/Examples", () => ({ + default: () => ({ type: "Examples" }), +})); +vi.mock("../pages/kitchensink/Gantt", () => ({ + default: () => ({ type: "Gantt" }), +})); +vi.mock("../pages/kitchensink/KitchenSink", () => ({ + default: () => ({ type: "KitchenSink" }), +})); +vi.mock("../pages/kitchensink/ThemeSampler", () => ({ + default: () => ({ type: "ThemeSampler" }), +})); +vi.mock("../pages/queueMonitor/TaskQueue", () => ({ + default: () => ({ type: "TaskQueue" }), +})); +vi.mock("../pages/scheduler", () => ({ + Schedule: () => ({ type: "Schedule" }), +})); + +// Mock react-router +vi.mock("react-router", () => ({ + createBrowserRouter: vi.fn(() => ({ type: "BrowserRouter" })), + Link: ({ to, children, ...props }: any) => ({ + type: "Link", + props: { to, children, ...props }, + }), +})); + +// Mock react-vis-timeline to avoid ES module issues +vi.mock("react-vis-timeline", () => ({ + default: () => ({ type: "Timeline" }), +})); + +// Import after mocks +import { router } from "../router"; +import { getRoutes } from "../routes"; + +/** + * Tests conductor-ui's getRoutes() in isolation: OSS core routes only, with no + * plugins registered. orkes-conductor-ui adds routes (login, callbacks, hub, + * get-started, task execution, etc.) by registering plugins; those are not + * covered here. + */ +describe("router", () => { + let mockFeatureFlags: any; + + beforeEach(async () => { + // Get the mocked feature flags + const utils = await import("utils"); + mockFeatureFlags = utils.featureFlags; + vi.clearAllMocks(); + }); + + it("should create a browser router with routes from getRoutes", () => { + expect(router).toBeDefined(); + }); + + it("should export the router instance", () => { + expect(router).toBeDefined(); + }); + + describe("Route Structure Analysis", () => { + it("should have a single root route with App element", () => { + const routes = getRoutes(); + + expect(routes).toHaveLength(1); + expect(routes[0]).toHaveProperty("path", "/"); + expect(routes[0]).toHaveProperty("element"); + expect(routes[0]).toHaveProperty("children"); + }); + + it("should contain essential OSS routes with correct elements", () => { + const routes = getRoutes(); + const children = routes[0].children; + + // OSS core: error catch-all and runWorkflow; login/callbacks come from plugins + const errorRoute = children?.find((child: any) => child.path === "*"); + const runWorkflowRoute = children?.find( + (child: any) => child.path === "/runWorkflow", + ); + + expect(errorRoute).toBeDefined(); + expect(runWorkflowRoute).toBeDefined(); + expect(errorRoute?.element).toBeDefined(); + expect(runWorkflowRoute?.element).toBeDefined(); + }); + + it("should have AuthGuard protected routes", () => { + const routes = getRoutes(); + const children = routes[0].children; + + // Find AuthGuard routes (routes with AuthGuard element) + const authGuardRoutes = children?.filter( + (child: any) => child.element && child.element.type === "AuthGuard", + ); + + expect(authGuardRoutes).toBeDefined(); + + // The routes structure might have AuthGuard routes or the routes might be structured differently + // Let's check if we have the expected authentication-related routes instead + const runWorkflowRoute = children?.find( + (child: any) => child.path === "/runWorkflow", + ); + expect(runWorkflowRoute).toBeDefined(); + + // OSS: children are AuthGuard group, runWorkflow, catch-all (*) + expect(children?.length).toBeGreaterThanOrEqual(3); + }); + + it("should have routes with dynamic parameters and correct elements", () => { + const routes = getRoutes(); + + // Flatten all routes to find dynamic ones + const allRoutes: any[] = []; + const flattenRoutes = (routeList: any[]) => { + routeList.forEach((route) => { + allRoutes.push(route); + if (route.children) { + flattenRoutes(route.children); + } + }); + }; + + flattenRoutes(routes); + + const dynamicRoutes = allRoutes.filter( + (route) => route.path && route.path.includes(":"), + ); + + expect(dynamicRoutes.length).toBeGreaterThan(0); + + // Check for specific dynamic routes with their expected elements + const executionRoute = allRoutes.find( + (route) => route.path === "/execution/:id/:taskId?", + ); + expect(executionRoute).toBeDefined(); + expect(executionRoute?.element).toBeDefined(); + + // Check workflow definition route + const workflowDefRoute = allRoutes.find( + (route) => + route.path && route.path.includes("/workflowDef/:name/:version"), + ); + expect(workflowDefRoute).toBeDefined(); + expect(workflowDefRoute?.element).toBeDefined(); + + // Check task definition route + const taskDefRoute = allRoutes.find( + (route) => route.path && route.path.includes("/taskDef/:name"), + ); + expect(taskDefRoute).toBeDefined(); + expect(taskDefRoute?.element).toBeDefined(); + + // Verify dynamic routes have proper parameter patterns + const parameterRoutes = dynamicRoutes.filter((route) => { + const path = route.path; + return ( + path.includes(":id") || + path.includes(":name") || + path.includes(":version") + ); + }); + expect(parameterRoutes.length).toBeGreaterThan(5); + }); + + it("should have wildcard routes for nested routing", () => { + const routes = getRoutes(); + + // Flatten all routes to find wildcard ones + const allRoutes: any[] = []; + const flattenRoutes = (routeList: any[]) => { + routeList.forEach((route) => { + allRoutes.push(route); + if (route.children) { + flattenRoutes(route.children); + } + }); + }; + + flattenRoutes(routes); + + const wildcardRoutes = allRoutes.filter( + (route) => + route.path && (route.path.includes("*") || route.path.includes("/*")), + ); + + expect(wildcardRoutes.length).toBeGreaterThan(0); + }); + + it("should have kitchen sink development routes with correct elements", () => { + const routes = getRoutes(); + + // Flatten all routes + const allRoutes: any[] = []; + const flattenRoutes = (routeList: any[]) => { + routeList.forEach((route) => { + allRoutes.push(route); + if (route.children) { + flattenRoutes(route.children); + } + }); + }; + + flattenRoutes(routes); + + const kitchenRoutes = allRoutes.filter( + (route) => route.path && route.path.includes("/kitchen"), + ); + + expect(kitchenRoutes.length).toBeGreaterThan(0); + + // Check for specific kitchen routes with their elements + const kitchenSinkRoute = allRoutes.find( + (route) => route.path === "/kitchen", + ); + const examplesRoute = allRoutes.find( + (route) => route.path === "/kitchen/examples", + ); + const ganttRoute = allRoutes.find( + (route) => route.path === "/kitchen/gantt", + ); + const themeRoute = allRoutes.find( + (route) => route.path === "/kitchen/theme", + ); + + expect(kitchenSinkRoute).toBeDefined(); + expect(kitchenSinkRoute?.element).toBeDefined(); + + expect(examplesRoute).toBeDefined(); + expect(examplesRoute?.element).toBeDefined(); + + expect(ganttRoute).toBeDefined(); + expect(ganttRoute?.element).toBeDefined(); + + expect(themeRoute).toBeDefined(); + expect(themeRoute?.element).toBeDefined(); + + // Verify all kitchen routes have elements + kitchenRoutes.forEach((route) => { + expect(route.element).toBeDefined(); + expect(route.path).toContain("/kitchen"); + }); + }); + + it("should have a substantial number of routes", () => { + const routes = getRoutes(); + + // Count all routes + let totalRoutes = 0; + const countRoutes = (routeList: any[]) => { + routeList.forEach((route) => { + totalRoutes++; + if (route.children) { + countRoutes(route.children); + } + }); + }; + + countRoutes(routes); + + // OSS core only (no plugins): still a substantial set of routes + expect(totalRoutes).toBeGreaterThan(25); + }); + + it("should have valid route structure with no duplicate paths at same level", () => { + const routes = getRoutes(); + + const checkDuplicates = (routeList: any[], level = 0) => { + const paths = routeList + .filter((route) => route.path) + .map((route) => route.path); + + const uniquePaths = new Set(paths); + expect(paths.length).toBe(uniquePaths.size); + + // Check children recursively + routeList.forEach((route) => { + if (route.children) { + checkDuplicates(route.children, level + 1); + } + }); + }; + + checkDuplicates(routes); + }); + + it("should have elements for all routes", () => { + const routes = getRoutes(); + + const validateRoutes = (routeList: any[]) => { + routeList.forEach((route) => { + expect(route).toHaveProperty("element"); + if (route.children) { + validateRoutes(route.children); + } + }); + }; + + validateRoutes(routes); + }); + }); + + describe("Feature Flag Conditional Routes", () => { + // Shared helper function to count all routes recursively + const countAllRoutes = (routes: any[]): number => { + let count = 0; + routes.forEach((route) => { + count++; + if (route.children) { + count += countAllRoutes(route.children); + } + }); + return count; + }; + + // Calculate baseline route count (all feature flags disabled) + let BASELINE_ROUTE_COUNT: number; + beforeAll(() => { + mockFeatureFlags.isEnabled.mockImplementation(() => false); + const baselineRoutes = getRoutes(); + BASELINE_ROUTE_COUNT = countAllRoutes(baselineRoutes); + }); + + it("should toggle PLAYGROUND feature flag and compare route counts", () => { + // In OSS-only, PLAYGROUND only affects getIndexRoute: when true, no index route is added (hub comes from plugins). + mockFeatureFlags.isEnabled.mockImplementation(() => false); + const routesPlaygroundDisabled = getRoutes(); + const countPlaygroundDisabled = countAllRoutes(routesPlaygroundDisabled); + + mockFeatureFlags.isEnabled.mockImplementation( + (feature: string) => feature === "PLAYGROUND", + ); + const routesPlaygroundEnabled = getRoutes(); + const countPlaygroundEnabled = countAllRoutes(routesPlaygroundEnabled); + + // PLAYGROUND true => one fewer route (no index route in OSS) + expect(countPlaygroundEnabled).toBe(countPlaygroundDisabled - 1); + expect(countPlaygroundDisabled).toBe(BASELINE_ROUTE_COUNT); + expect(countPlaygroundEnabled).toBe(BASELINE_ROUTE_COUNT - 1); + }); + + it("should call feature flags when getRoutes runs (not at module load)", () => { + // Feature flags are read inside getRoutes(), not at routes module load + vi.clearAllMocks(); + getRoutes(); + expect(mockFeatureFlags.isEnabled).toHaveBeenCalled(); + }); + + it("should show what routes are actually generated with current feature flag settings", () => { + const routes = getRoutes(); + + // Flatten all routes to see what we actually get + const getAllPaths = (routes: any[]): string[] => { + const paths: string[] = []; + routes.forEach((route) => { + if (route.path) { + paths.push(route.path); + } + if (route.children) { + paths.push(...getAllPaths(route.children)); + } + }); + return paths; + }; + + const allPaths = getAllPaths(routes); + + // Check for conditional paths that should/shouldn't be there + const conditionalPaths = { + hub: allPaths.filter((path) => path.includes("/hub")), + getStarted: allPaths.filter((path) => path.includes("/get-started")), + taskExecution: allPaths.filter((path) => + path.includes("/taskExecution"), + ), + }; + + // OSS-only: no hub, get-started, or taskExecution (those come from plugins) + expect(conditionalPaths.hub.length).toBe(0); + expect(conditionalPaths.getStarted.length).toBe(0); + expect(conditionalPaths.taskExecution.length).toBe(0); + + // OSS core routes + expect(allPaths).toContain("*"); + expect(allPaths).toContain("/executions"); + expect(allPaths).toContain("/runWorkflow"); + }); + + it("should not change route count for SHOW_GET_STARTED_PAGE in OSS", () => { + // get-started route is added by orkes plugins, not by conductor-ui getRoutes() + mockFeatureFlags.isEnabled.mockImplementation(() => false); + const countDisabled = countAllRoutes(getRoutes()); + mockFeatureFlags.isEnabled.mockImplementation( + (feature: string) => feature === "SHOW_GET_STARTED_PAGE", + ); + const countEnabled = countAllRoutes(getRoutes()); + expect(countEnabled).toBe(countDisabled); + expect(countDisabled).toBe(BASELINE_ROUTE_COUNT); + }); + + it("should not change route count for TASK_INDEXING in OSS", () => { + // taskExecution route is added by orkes plugins, not by conductor-ui getRoutes() + mockFeatureFlags.isEnabled.mockImplementation(() => false); + const countDisabled = countAllRoutes(getRoutes()); + mockFeatureFlags.isEnabled.mockImplementation( + (feature: string) => feature === "TASK_INDEXING", + ); + const countEnabled = countAllRoutes(getRoutes()); + expect(countEnabled).toBe(countDisabled); + expect(countDisabled).toBe(BASELINE_ROUTE_COUNT); + }); + + it("should only change count for PLAYGROUND when multiple flags toggled in OSS", () => { + // In OSS, only PLAYGROUND affects getRoutes() (drops index route). Others are plugin-driven. + mockFeatureFlags.isEnabled.mockImplementation(() => false); + const countAllDisabled = countAllRoutes(getRoutes()); + mockFeatureFlags.isEnabled.mockImplementation((feature: string) => + ["PLAYGROUND", "SHOW_GET_STARTED_PAGE", "TASK_INDEXING"].includes( + feature, + ), + ); + const countAllEnabled = countAllRoutes(getRoutes()); + expect(countAllEnabled).toBe(countAllDisabled - 1); // PLAYGROUND removes index route + expect(countAllDisabled).toBe(BASELINE_ROUTE_COUNT); + expect(countAllEnabled).toBe(BASELINE_ROUTE_COUNT - 1); + }); + + it("should reflect OSS behavior: only PLAYGROUND changes count", () => { + mockFeatureFlags.isEnabled.mockImplementation(() => false); + const countAllDisabled = countAllRoutes(getRoutes()); + + mockFeatureFlags.isEnabled.mockImplementation( + (feature: string) => feature === "PLAYGROUND", + ); + const countPlaygroundOnly = countAllRoutes(getRoutes()); + + mockFeatureFlags.isEnabled.mockImplementation( + (feature: string) => feature === "SHOW_GET_STARTED_PAGE", + ); + const countGetStartedOnly = countAllRoutes(getRoutes()); + + mockFeatureFlags.isEnabled.mockImplementation( + (feature: string) => feature === "TASK_INDEXING", + ); + const countTaskIndexingOnly = countAllRoutes(getRoutes()); + + // Only PLAYGROUND changes count in conductor-ui (-1 for no index route) + expect(countPlaygroundOnly).toBe(countAllDisabled - 1); + expect(countGetStartedOnly).toBe(countAllDisabled); + expect(countTaskIndexingOnly).toBe(countAllDisabled); + expect(countAllDisabled).toBe(BASELINE_ROUTE_COUNT); + }); + }); +}); diff --git a/ui-next/src/routes/__tests__/test-utils.tsx b/ui-next/src/routes/__tests__/test-utils.tsx new file mode 100644 index 0000000000..176c64a2e7 --- /dev/null +++ b/ui-next/src/routes/__tests__/test-utils.tsx @@ -0,0 +1,298 @@ +import { vi } from "vitest"; +import { FEATURES } from "utils"; + +/** + * Mock feature flags utility + */ +export const createMockFeatureFlags = (enabledFeatures: string[] = []) => { + return { + isEnabled: vi.fn((feature: string) => enabledFeatures.includes(feature)), + getValue: vi.fn((feature: string, defaultValue?: string) => defaultValue), + getContextValue: vi.fn(() => undefined), + }; +}; + +/** + * Common feature flag configurations for testing + */ +export const FEATURE_FLAG_SCENARIOS = { + PLAYGROUND_ENABLED: [FEATURES.PLAYGROUND], + GET_STARTED_ENABLED: [FEATURES.SHOW_GET_STARTED_PAGE], + TASK_INDEXING_ENABLED: [FEATURES.TASK_INDEXING], + ALL_FEATURES_ENABLED: [ + FEATURES.PLAYGROUND, + FEATURES.SHOW_GET_STARTED_PAGE, + FEATURES.TASK_INDEXING, + FEATURES.SCHEDULER, + FEATURES.HUMAN_TASK, + FEATURES.SECRETS, + FEATURES.WEBHOOKS, + FEATURES.RBAC, + FEATURES.INTEGRATIONS, + FEATURES.REMOTE_SERVICES, + ], + NO_FEATURES_ENABLED: [], +}; + +/** + * Mock all page components with consistent test IDs + */ +export const mockPageComponents = () => { + // Mock Okta components + vi.mock("@okta/okta-react", () => ({ + LoginCallback: () =>
    LoginCallback
    , + })); + + // Mock core components + vi.mock("components/auth/AuthGuard", () => ({ + default: ({ children, runWorkflow }: any) => ( +
    + {children} +
    + ), + })); + + vi.mock("components/App", () => ({ + App: ({ children }: any) =>
    {children}
    , + })); + + // Mock auth components + vi.mock("shared/auth/oidc/OidcRedirectEndpoint", () => ({ + OidcRedirectEndpoint: () => ( +
    OidcRedirectEndpoint
    + ), + })); + + vi.mock("enterprise/pages/auth/Login", () => ({ + default: () =>
    Login
    , + })); + + // Mock page components + const pageComponentMocks = { + // Access management + "enterprise/pages/access/ApplicationManagement": "application-management", + "enterprise/pages/access/GroupManagement": "group-management", + "enterprise/pages/access/users/UserManagement": "user-management", + + // AI and integrations + "enterprise/pages/aiPrompts/AiPromptsManagement": "ai-prompts-management", + "enterprise/pages/integrations/IntegrationsManagement": + "integrations-management", + + // Authentication + "enterprise/pages/Authentication/AuthListing": "auth-listing", + + // Definitions + "pages/definition/task": { TaskDefinition: "task-definition" }, + "pages/definition/WorkflowDefinition": "workflow-definition", + "../pages/definition/EventHandler/EventHandler": "event-handler-definition", + + // Executions + "pages/executions": { + SchedulerExecutions: "scheduler-executions", + TaskSearch: "task-search", + WorkflowSearch: "workflow-search", + }, + "../pages/execution/Execution": "execution", + + // Hub + "enterprise/pages/hub/hub": { + HubMain: "hub-main", + HubTemplateDetail: "hub-template-detail", + HubTemplateImport: "hub-template-import", + }, + + // Human tasks + "enterprise/pages/human": { SearchPage: "search-page" }, + "enterprise/pages/human/humanTask": { TaskPage: "task-page" }, + "enterprise/pages/human/search/TaskInboxPage": { + TaskInboxPage: "task-inbox-page", + }, + "enterprise/pages/human/templates": { + TemplateEditorPage: "template-editor-page", + TemplatePage: "template-page", + }, + + // Other pages + "pages/creatorFlags/CreatorFlags": { CreatorFlags: "creator-flags" }, + "enterprise/pages/envVariables/EnvVariables": { + EnvVariables: "env-variables", + }, + "pages/error/ErrorPage": "error-page", + "pages/eventMonitor/EventMonitor": { EventMonitor: "event-monitor" }, + "pages/eventMonitor/EventMonitorDetail/EventMonitorDetail": { + EventMonitorDetail: "event-monitor-detail", + }, + "enterprise/pages/getStarted/GetStarted": "get-started", + "enterprise/pages/metrics": "metrics-page", + "enterprise/pages/secrets/Secrets": "secrets", + "enterprise/pages/workflowExplorer/Explorer": "explorer", + + // Remote services + "enterprise/pages/remoteServices/edit/ServiceEdit": "service-edit", + "enterprise/pages/remoteServices/Services": { Services: "remote-services" }, + + // Schema + "enterprise/pages/schema/edit/SchemaEditPage": { + SchemaEditPage: "schema-edit-page", + }, + "enterprise/pages/schema/list/SchemaList": { SchemaList: "schema-list" }, + + // Services + "enterprise/pages/services/EditService": "edit-service", + "enterprise/pages/services/NewService": "new-service", + "enterprise/pages/services/routes/EditRoute": "edit-route", + "enterprise/pages/services/routes/NewRoute": "new-route", + "enterprise/pages/services/routes/RouteDetails": "route-details", + "enterprise/pages/services/Service": "service", + "enterprise/pages/services/Services": "services", + + // Webhooks + "enterprise/pages/webhooks": { Webhooks: "webhooks" }, + "enterprise/pages/webhooks/edit/WebhookEdit": { + WebhookEditPage: "webhook-edit-page", + }, + + // Kitchen sink + "../pages/kitchensink/Examples": "examples", + "../pages/kitchensink/Gantt": "gantt", + "../pages/kitchensink/KitchenSink": "kitchen-sink", + "../pages/kitchensink/ThemeSampler": "theme-sampler", + + // Queue and scheduler + "../pages/queueMonitor/TaskQueue": "task-queue", + "../pages/scheduler": { Schedule: "schedule" }, + + // Definitions (additional) + "pages/definitions": { + EventHandler: "event-handler-definitions", + Schedules: "schedule-definitions", + Task: "task-definitions", + Workflow: "workflow-definitions", + }, + }; + + // Create mocks for each page component + Object.entries(pageComponentMocks).forEach(([path, mockConfig]) => { + if (typeof mockConfig === "string") { + // Simple default export + vi.mock(path, () => ({ + default: () =>
    {mockConfig}
    , + })); + } else { + // Named exports + const namedExports: any = {}; + Object.entries(mockConfig).forEach(([exportName, testId]) => { + namedExports[exportName] = () => ( +
    {testId}
    + ); + }); + vi.mock(path, () => namedExports); + } + }); +}; + +/** + * Helper to find routes in the route tree + */ +export const findRouteByPath = (routes: any[], path: string): any => { + for (const route of routes) { + if (route.path === path) { + return route; + } + if (route.children) { + const found = findRouteByPath(route.children, path); + if (found) return found; + } + } + return null; +}; + +/** + * Helper to find all routes with a specific property + */ +export const findRoutesByProperty = ( + routes: any[], + property: string, + value?: any, +): any[] => { + const found: any[] = []; + + for (const route of routes) { + if (value !== undefined) { + if (route[property] === value) { + found.push(route); + } + } else { + if (Object.hasOwn(route, property)) { + found.push(route); + } + } + + if (route.children) { + found.push(...findRoutesByProperty(route.children, property, value)); + } + } + + return found; +}; + +/** + * Helper to get all paths from route tree + */ +export const getAllPaths = (routes: any[]): string[] => { + const paths: string[] = []; + + for (const route of routes) { + if (route.path) { + paths.push(route.path); + } + if (route.children) { + paths.push(...getAllPaths(route.children)); + } + } + + return paths; +}; + +/** + * Helper to count routes at each level + */ +export const getRouteStats = (routes: any[]) => { + let totalRoutes = 0; + let dynamicRoutes = 0; + let wildcardRoutes = 0; + let indexRoutes = 0; + + const countRoutes = (routeList: any[]) => { + for (const route of routeList) { + totalRoutes++; + + if (route.index) { + indexRoutes++; + } + + if (route.path) { + if (route.path.includes(":")) { + dynamicRoutes++; + } + if (route.path.includes("*")) { + wildcardRoutes++; + } + } + + if (route.children) { + countRoutes(route.children); + } + } + }; + + countRoutes(routes); + + return { + totalRoutes, + dynamicRoutes, + wildcardRoutes, + indexRoutes, + }; +}; diff --git a/ui-next/src/routes/router.tsx b/ui-next/src/routes/router.tsx new file mode 100644 index 0000000000..f7fd3498fd --- /dev/null +++ b/ui-next/src/routes/router.tsx @@ -0,0 +1,4 @@ +import { createBrowserRouter } from "react-router"; +import { getRoutes } from "./routes"; + +export const router = createBrowserRouter(getRoutes()); diff --git a/ui-next/src/routes/routes.tsx b/ui-next/src/routes/routes.tsx new file mode 100644 index 0000000000..f78308e8d9 --- /dev/null +++ b/ui-next/src/routes/routes.tsx @@ -0,0 +1,270 @@ +/** + * Routes Configuration + * + * This module defines the application routes. Core routes are defined inline, + * while enterprise routes are registered via the plugin system. + * + * Core routes (OSS): + * - Workflow definitions and executions + * - Task definitions + * - Event handlers + * - Scheduler definitions and executions + * - Queue monitor + * - Event monitor + * - API reference + * - Tags dashboard + * + * Enterprise routes (registered via plugins): + * - Auth (login, callbacks, RBAC pages) + * - Webhooks + * - Human Tasks + * - AI Prompts + * - Secrets + * - Integrations + * - Gateway Services + * - Remote Services + * - Metrics + * - Environment Variables + * - Schemas + * - Workers + */ + +import { App } from "components/App"; +import DefaultAuthGuard from "components/auth/AuthGuard"; +import ApiReferencePage from "pages/apiDocs/ApiReferencePage"; +import { CreatorFlags } from "pages/creatorFlags/CreatorFlags"; +import { TaskDefinition } from "pages/definition/task"; +import WorkflowDefinition from "pages/definition/WorkflowDefinition"; +import { + EventHandler as EventHandlerDefinitions, + Schedules as ScheduleDefinitions, + Task as TaskDefinitions, + Workflow as WorkflowDefinitions, +} from "pages/definitions"; +import ErrorPage from "pages/error/ErrorPage"; +import { EventMonitor } from "pages/eventMonitor/EventMonitor"; +import { EventMonitorDetail } from "pages/eventMonitor/EventMonitorDetail/EventMonitorDetail"; +import { SchedulerExecutions, WorkflowSearch } from "pages/executions"; +import { pluginRegistry } from "plugins/registry"; +import { featureFlags, FEATURES } from "utils"; +import { + API_REFERENCE_URL, + EVENT_HANDLERS_URL, + EVENT_MONITOR_URL, + NEW_TASK_DEF_URL, + RUN_WORKFLOW_URL, + SCHEDULER_DEFINITION_URL, + TAGS_DASHBOARD_URL, + TASK_DEF_URL, + TASK_QUEUE_URL, + WORKFLOW_DEFINITION_URL, +} from "utils/constants/route"; +import EventHandlerDefinition from "../pages/definition/EventHandler/EventHandler"; +import Execution from "../pages/execution/Execution"; +import Examples from "../pages/kitchensink/Examples"; +import Gantt from "../pages/kitchensink/Gantt"; +import KitchenSink from "../pages/kitchensink/KitchenSink"; +import ThemeSampler from "../pages/kitchensink/ThemeSampler"; +import TaskQueue from "../pages/queueMonitor/TaskQueue"; +import { Schedule } from "../pages/scheduler"; +import TagsDashboard from "pages/tags/TagsDashboard"; + +/** + * Core authenticated routes (OSS) + * These are the fundamental Conductor UI features available in open source. + */ +const getCoreAuthenticatedRoutes = () => [ + // Workflow Executions + { + path: "/executions", + element: , + }, + { + path: "/schedulerExecs", + element: , + }, + { + path: "/execution/:id/:taskId?", + element: , + }, + + // Workflow Definitions + { + path: WORKFLOW_DEFINITION_URL.BASE, + element: , + }, + { + path: WORKFLOW_DEFINITION_URL.NAME_VERSION, + element: , + }, + { + path: WORKFLOW_DEFINITION_URL.NEW, + element: , + }, + { + path: "/workFlowTemplate/:templateId", + element: , + }, + + // Task Definitions + { + path: NEW_TASK_DEF_URL, + element: , + }, + { + path: TASK_DEF_URL.BASE, + element: , + }, + { + path: TASK_DEF_URL.NAME, + element: , + }, + + // Event Handlers + { + path: EVENT_HANDLERS_URL.BASE, + element: , + }, + { + path: EVENT_HANDLERS_URL.NAME, + element: , + }, + { + path: EVENT_HANDLERS_URL.NEW, + element: , + }, + + // Scheduler Definitions + { + path: SCHEDULER_DEFINITION_URL.BASE, + element: , + }, + { + path: SCHEDULER_DEFINITION_URL.NAME, + element: , + }, + { + path: SCHEDULER_DEFINITION_URL.NEW, + element: , + }, + + // Queue Monitor + { + path: TASK_QUEUE_URL.BASE, + element: , + }, + + // Event Monitor + { + path: EVENT_MONITOR_URL.BASE, + element: , + }, + { + path: EVENT_MONITOR_URL.NAME, + element: , + }, + + // Tags Dashboard + { + path: TAGS_DASHBOARD_URL.BASE, + element: , + }, + + // API Reference + { + path: API_REFERENCE_URL.BASE, + element: , + }, + + // Dev/Debug pages (Kitchen Sink) + { + path: "/kitchen", + element: , + }, + { + path: "/kitchen/examples", + element: , + }, + { + path: "/kitchen/gantt", + element: , + }, + { + path: "/kitchen/theme", + element: , + }, + { + path: "/flags", + element: , + }, +]; + +/** + * Get the default index route based on feature flags + */ +const getIndexRoute = (isPlayground: boolean) => { + if (isPlayground) { + // In playground mode, we need the hub pages - these come from plugins + return null; // Will be provided by playground plugin + } + return { + index: true, + element: , + }; +}; + +/** + * Build the complete route configuration + */ +export const getRoutes = () => { + const isPlayground = featureFlags.isEnabled(FEATURES.PLAYGROUND); + + // Get routes from plugins + const pluginAuthenticatedRoutes = pluginRegistry.getRoutes(); + const pluginPublicRoutes = pluginRegistry.getPublicRoutes(); + + // Get auth guard from plugins (enterprise) or use default (OSS) + const AuthGuard = pluginRegistry.getAuthGuard() || DefaultAuthGuard; + + // Core authenticated routes + const coreRoutes = getCoreAuthenticatedRoutes(); + + // Build the index route (either core WorkflowSearch or from playground plugin) + const indexRoute = getIndexRoute(isPlayground); + + // Combine all authenticated routes + const allAuthenticatedRoutes = [ + ...(indexRoute ? [indexRoute] : []), + ...coreRoutes, + ...pluginAuthenticatedRoutes, + ]; + + return [ + { + path: "/", + element: , + children: [ + // Main authenticated section + { + element: , + children: allAuthenticatedRoutes, + }, + + // Special route for runWorkflow (has special AuthGuard behavior) + { + path: RUN_WORKFLOW_URL, + element: , + }, + + // Public routes from plugins (login pages, OAuth callbacks, etc.) + ...pluginPublicRoutes, + + // Error page (catch-all) + { + path: "*", + element: , + }, + ], + }, + ]; +}; diff --git a/ui-next/src/setupTests.ts b/ui-next/src/setupTests.ts new file mode 100644 index 0000000000..ee0cdb94c4 --- /dev/null +++ b/ui-next/src/setupTests.ts @@ -0,0 +1,19 @@ +import "@testing-library/jest-dom"; +import { vi } from "vitest"; + +// Monaco Editor calls document.queryCommandSupported during module init, +// which jsdom does not implement. Stub it out globally. +Object.defineProperty(document, "queryCommandSupported", { + value: vi.fn(() => false), + writable: true, +}); + +// Monaco Editor does not run in jsdom. Mock the package so tests that render +// components containing editors get a lightweight no-op instead. +vi.mock("@monaco-editor/react", () => ({ + default: vi.fn(() => null), + Editor: vi.fn(() => null), + DiffEditor: vi.fn(() => null), + useMonaco: vi.fn(() => null), + loader: { config: vi.fn() }, +})); diff --git a/ui-next/src/shared/BaseLayout.tsx b/ui-next/src/shared/BaseLayout.tsx new file mode 100644 index 0000000000..ecd6e1f1c0 --- /dev/null +++ b/ui-next/src/shared/BaseLayout.tsx @@ -0,0 +1,148 @@ +/** + * BaseLayout — the OSS sidebar + top bar layout with no enterprise dependencies. + * + * The enterprise `additional` plugin replaces this with an agent-aware wrapper + * by registering an `appLayout` component via the plugin registry. + */ + +import { AppBar, Box, Toolbar } from "@mui/material"; +import SearchEverythingModal from "components/searchWrapper/SearchWrapper"; +import { SidebarContext } from "components/Sidebar/context/SidebarContext"; +import AnnouncementBanner from "components/v1/layout/header/AnnouncementBanner"; +import { ReactNode, useContext } from "react"; +import { UISidebar } from "components/Sidebar/UiSidebar"; +import { releaseVersion } from "utils/releaseVersion"; +import AppBarModules from "../plugins/AppBarModules"; +import { useAuth } from "./auth"; + +const apiVersion = localStorage.getItem("version"); +const toolBarHeight = 60; + +type Props = { + children: ReactNode; +}; + +export const BaseLayout = ({ children }: Props) => { + const { + toggleMenu, + isMobile, + hideSideBar, + isBannerOpen, + setBannerOpen, + isSearchModalOpen, + setSearchModal, + showAiStudioBanner, + dismissAiStudioBanner, + } = useContext(SidebarContext); + const { + isTrialExpired, + trialExpiryDate, + isAnnouncementBannerDismissed, + dismissAnnouncementBanner, + } = useAuth(); + + return ( + + + + + {hideSideBar ? null : ( + + )} + + {isSearchModalOpen && ( + + )} + + + {!isMobile ? null : ( + + + + + + + + )} + + + {children} + + + ); +}; + +export default BaseLayout; diff --git a/ui-next/src/shared/BlockNavigationWithConfirmation.tsx b/ui-next/src/shared/BlockNavigationWithConfirmation.tsx new file mode 100644 index 0000000000..85ef168072 --- /dev/null +++ b/ui-next/src/shared/BlockNavigationWithConfirmation.tsx @@ -0,0 +1,110 @@ +import UnsavedChangesDialog from "components/v1/Modal/UnsavedChangesDialog"; +import { ReactNode, useRef, useState } from "react"; +import { useBlocker, useNavigate } from "react-router"; + +export interface BlockNavigationWithConfirmationProps { + nonBlockPaths: string[]; + promptMessage?: ReactNode; + title?: ReactNode; + block?: boolean; + hasErrors?: boolean; + onSave?: () => void; + successfulSave?: boolean; + onDiscard?: () => void; +} + +const isAcceptedPath = ( + nonBlockPaths: string[], + currentPathWithQuery: string, +) => { + return nonBlockPaths.some((path) => { + const regExMatching = new RegExp(path); + return regExMatching.test(currentPathWithQuery); + }); +}; + +const BlockNavigationWithConfirmation = ({ + nonBlockPaths, + block, + title, + hasErrors = false, + onSave, + successfulSave, + onDiscard, +}: BlockNavigationWithConfirmationProps) => { + const navigate = useNavigate(); + const [targetLocation, setTargetLocation] = useState(null); + const isDiscardingRef = useRef(false); + const shouldBlock = block !== false && !isDiscardingRef.current; // Block unless discarding + + const handleAction = () => { + onSave?.(); + }; + + const handleCancel = () => { + setTargetLocation(null); + }; + + const handleDiscard = () => { + // Call the onDiscard callback if provided (e.g., to cancel stream and clear messages) + onDiscard?.(); + + if (targetLocation) { + const locationToNavigate = targetLocation; + isDiscardingRef.current = true; // Temporarily disable blocker + setTargetLocation(null); + // Use setTimeout to ensure the blocker is disabled before navigating + setTimeout(() => { + navigate(locationToNavigate); + isDiscardingRef.current = false; // Re-enable blocker after navigation + }, 0); + } + }; + + // Handle successful save navigation using a ref to track previous state + const prevSuccessfulSaveRef = useRef(successfulSave); + + if ( + successfulSave !== undefined && + successfulSave !== prevSuccessfulSaveRef.current && + successfulSave + ) { + // If there's a pending navigation, navigate to it + if (targetLocation) { + navigate(targetLocation); + } + // Close the dialog after successful save (regardless of navigation) + setTargetLocation(null); + } + prevSuccessfulSaveRef.current = successfulSave; + + useBlocker(({ nextLocation }) => { + if (!block || !shouldBlock) return false; + + const fullLocation = nextLocation.pathname + nextLocation.search; + if (!isAcceptedPath(nonBlockPaths, nextLocation.pathname)) { + setTargetLocation(fullLocation); + return true; // Block navigation + } + return false; // Allow navigation + }); + + return ( + + ); +}; + +export default BlockNavigationWithConfirmation; diff --git a/ui-next/src/shared/CodeModal/curlHeader.ts b/ui-next/src/shared/CodeModal/curlHeader.ts new file mode 100644 index 0000000000..65438f558b --- /dev/null +++ b/ui-next/src/shared/CodeModal/curlHeader.ts @@ -0,0 +1,4 @@ +export const curlHeaders = (accessToken: string) => ({ + Accept: "*/*", + "X-Authorization": accessToken, +}); diff --git a/ui-next/src/shared/CodeModal/hook.ts b/ui-next/src/shared/CodeModal/hook.ts new file mode 100644 index 0000000000..162b1c8ab5 --- /dev/null +++ b/ui-next/src/shared/CodeModal/hook.ts @@ -0,0 +1,26 @@ +import { useState } from "react"; +import { SupportedDisplayTypes } from "./types"; +import _prop from "lodash/property"; +import { getAccessToken } from "shared/auth/tokenManagerJotai"; + +export type toCodeT = Partial< + Record string> +>; + +export const useParamsToSdk = (args: T, toCode: toCodeT) => { + const [selectedLanguage, setSelectedLanguage] = + useState("curl"); + + const toCodeFunc = _prop< + toCodeT, + (args: T, accessToken: string) => string + >(selectedLanguage)(toCode); + + const accessToken = getAccessToken() || ""; + + return { + selectedLanguage, + setSelectedLanguage, + code: toCodeFunc(args, accessToken), + }; +}; diff --git a/ui-next/src/shared/CodeModal/types.ts b/ui-next/src/shared/CodeModal/types.ts new file mode 100644 index 0000000000..9b661ea6f8 --- /dev/null +++ b/ui-next/src/shared/CodeModal/types.ts @@ -0,0 +1,11 @@ +export type SupportedDisplayTypes = "javascript" | "java" | "curl" | "python"; + +export type ApiSearchModalProps = { + dialogTitle?: string; + dialogHeaderText?: string; + code: string; + handleClose: () => void; + onTabChange: (selectedType: SupportedDisplayTypes) => void; + displayLanguage: SupportedDisplayTypes; + languages: SupportedDisplayTypes[]; +}; diff --git a/ui-next/src/shared/PersistableSidebar/state/actions.ts b/ui-next/src/shared/PersistableSidebar/state/actions.ts new file mode 100644 index 0000000000..c1e29dc59d --- /dev/null +++ b/ui-next/src/shared/PersistableSidebar/state/actions.ts @@ -0,0 +1,88 @@ +import { DoneInvokeEvent, assign } from "xstate"; +import { + AddMenuEventType, + AddMenusEventType, + RemoveMenuEventType, + SidebarMachineContext, + ToggleBannerEventType, + ToggleSearchModalEventType, +} from "./types"; +import _filter from "lodash/filter"; +import { MENU_ITEMS_LOCAL_STORAGE_KEY } from "./services"; + +export const persistOpenedMenus = assign({ + openedMenus: (_context, { data }: DoneInvokeEvent) => data, +}); + +export const persistBannerStateInContext = assign< + SidebarMachineContext, + ToggleBannerEventType +>((_context, { data }) => ({ + isBannerOpen: data.val, +})); + +export const persistSearchModalStateInContext = assign< + SidebarMachineContext, + ToggleSearchModalEventType +>((_context, { data }) => ({ + isSearchModalOpen: data.val, +})); + +export const clearMenuState = assign( + ({ openedMenus }) => ({ + stashedMenus: openedMenus, + openedMenus: [], + }), +); + +export const restoreMenuState = assign( + ({ stashedMenus }) => ({ + openedMenus: stashedMenus, + stashedMenus: [], + }), +); + +export const addInOpenedMenus = assign({ + openedMenus: ( + { openedMenus }: SidebarMachineContext, + { data }: AddMenuEventType, + ) => { + const { id } = data; + const existedMenu = openedMenus?.includes(id); + if (!existedMenu) { + const updatedMenus = [...openedMenus, id]; + localStorage.setItem( + MENU_ITEMS_LOCAL_STORAGE_KEY, + JSON.stringify(updatedMenus), + ); + return updatedMenus; + } + return openedMenus; + }, +}); + +export const removeFromOpenedMenus = assign({ + openedMenus: ( + { openedMenus }: SidebarMachineContext, + { data }: RemoveMenuEventType, + ) => { + const { id } = data; + const updatedMenus = _filter(openedMenus, (menu) => menu !== id); + localStorage.setItem( + MENU_ITEMS_LOCAL_STORAGE_KEY, + JSON.stringify(updatedMenus), + ); + return updatedMenus; + }, +}); + +export const setOpenedMenus = assign({ + openedMenus: ( + _context: SidebarMachineContext, + { data }: AddMenusEventType, + ) => { + const { items } = data; + localStorage.setItem(MENU_ITEMS_LOCAL_STORAGE_KEY, JSON.stringify(items)); + return items; + }, +}); diff --git a/ui-next/src/shared/PersistableSidebar/state/hook.ts b/ui-next/src/shared/PersistableSidebar/state/hook.ts new file mode 100644 index 0000000000..f23e8ec53d --- /dev/null +++ b/ui-next/src/shared/PersistableSidebar/state/hook.ts @@ -0,0 +1,117 @@ +import { useSelector } from "@xstate/react"; +import { useCallback, useEffect } from "react"; +import { useLocation } from "react-router"; +import { ActorRef, State } from "xstate"; +import { + PersistableSidebarEvent, + PersistableSidebarEventTypes, + PersistableSidebarStates, + SidebarMachineContext, +} from "./types"; + +export const useSidebarMenu = ( + sidebarActor: ActorRef, + isMobile: boolean, +) => { + const location = useLocation(); + + const isSidebarExpanded = useSelector( + sidebarActor, + (state: State) => + state.matches(PersistableSidebarStates.EXPANDED), + ); + + const openedMenus = useSelector( + sidebarActor!, + (state: State) => state.context.openedMenus, + ); + + const isBannerOpen = useSelector( + sidebarActor!, + (state: State) => state.context.isBannerOpen, + ); + + const isSearchModalOpen = useSelector( + sidebarActor!, + (state: State) => state.context.isSearchModalOpen, + ); + + const handleAnnouncementBanner = (val: boolean) => { + sidebarActor.send({ + type: PersistableSidebarEventTypes.TOGGLE_BANNER_EVENT, + data: { val }, + }); + }; + + const handleSearchModal = (val: boolean) => { + sidebarActor.send({ + type: PersistableSidebarEventTypes.TOGGLE_SEARCH_MODAL_EVENT, + data: { val }, + }); + }; + + const expandSidebar = useCallback(() => { + sidebarActor.send({ + type: PersistableSidebarEventTypes.EXPAND_SIDEBAR_EVENT, + }); + }, [sidebarActor]); + + const collapseSidebar = useCallback(() => { + sidebarActor?.send({ + type: PersistableSidebarEventTypes.COLLAPSE_SIDEBAR_EVENT, + }); + }, [sidebarActor]); + + const addMenu = (id: string) => { + sidebarActor.send({ + type: PersistableSidebarEventTypes.ADD_MENU_EVENT, + data: { id }, + }); + }; + + const removeMenu = (id: string) => { + sidebarActor.send({ + type: PersistableSidebarEventTypes.REMOVE_MENU_EVENT, + data: { id }, + }); + }; + + const setOpenedMenus = (items: string[]) => { + sidebarActor.send({ + type: PersistableSidebarEventTypes.ADD_MENUS_EVENT, + data: { items }, + }); + }; + + const toggleSidebar = () => { + if (isSidebarExpanded) { + collapseSidebar(); + } else { + expandSidebar(); + } + }; + + useEffect(() => { + if (isMobile) { + collapseSidebar(); + } + }, [collapseSidebar, isMobile, location.pathname]); + + return { + openedMenus, + isSidebarHidden: + location.pathname === "/integrations/addIntegration" || + location.pathname === "/login", + isBannerOpen, + isSearchModalOpen, + location, + isSidebarExpanded, + handleAnnouncementBanner, + handleSearchModal, + collapseSidebar, + toggleSidebar, + addMenu, + removeMenu, + setOpenedMenus, + }; +}; diff --git a/ui-next/src/shared/PersistableSidebar/state/machine.ts b/ui-next/src/shared/PersistableSidebar/state/machine.ts new file mode 100644 index 0000000000..370bce0568 --- /dev/null +++ b/ui-next/src/shared/PersistableSidebar/state/machine.ts @@ -0,0 +1,77 @@ +import { createMachine } from "xstate"; +import { + PersistableSidebarEvent, + PersistableSidebarEventTypes, + PersistableSidebarStates, + SidebarMachineContext, +} from "./types"; +import * as actions from "./actions"; +import * as services from "./services"; + +export const sidebarMachine = createMachine< + SidebarMachineContext, + PersistableSidebarEvent +>( + { + id: "sidebarMachine", + predictableActionArguments: true, + initial: PersistableSidebarStates.INIT, + context: { + openedMenus: [], + stashedMenus: [], + isBannerOpen: true, + isSearchModalOpen: false, + }, + on: { + [PersistableSidebarEventTypes.TOGGLE_BANNER_EVENT]: { + actions: "persistBannerStateInContext", + }, + [PersistableSidebarEventTypes.TOGGLE_SEARCH_MODAL_EVENT]: { + actions: "persistSearchModalStateInContext", + }, + [PersistableSidebarEventTypes.ADD_MENU_EVENT]: { + actions: "addInOpenedMenus", + }, + [PersistableSidebarEventTypes.REMOVE_MENU_EVENT]: { + actions: "removeFromOpenedMenus", + }, + [PersistableSidebarEventTypes.ADD_MENUS_EVENT]: { + actions: "setOpenedMenus", + }, + }, + states: { + [PersistableSidebarStates.INIT]: { + invoke: { + src: "getOpenedMenusFromLocalStorage", + onDone: { + target: PersistableSidebarStates.EXPANDED, + actions: "persistOpenedMenus", + }, + onError: { + target: PersistableSidebarStates.EXPANDED, + }, + }, + }, + [PersistableSidebarStates.EXPANDED]: { + on: { + [PersistableSidebarEventTypes.COLLAPSE_SIDEBAR_EVENT]: { + actions: "clearMenuState", + target: PersistableSidebarStates.COLLAPSED, + }, + }, + }, + [PersistableSidebarStates.COLLAPSED]: { + on: { + [PersistableSidebarEventTypes.EXPAND_SIDEBAR_EVENT]: { + actions: "restoreMenuState", + target: PersistableSidebarStates.EXPANDED, + }, + }, + }, + }, + }, + { + actions: actions as any, + services: services as any, + }, +); diff --git a/ui-next/src/shared/PersistableSidebar/state/services.ts b/ui-next/src/shared/PersistableSidebar/state/services.ts new file mode 100644 index 0000000000..976962ec97 --- /dev/null +++ b/ui-next/src/shared/PersistableSidebar/state/services.ts @@ -0,0 +1,28 @@ +import { tryToJson } from "utils/utils"; + +export const MENU_ITEMS_LOCAL_STORAGE_KEY = "menuItems"; + +const defaultOpenedMenus = [ + "executionsSubMenu", + "definitionsSubMenu", + "adminSubMenu", +]; + +export const getOpenedMenusFromLocalStorage = async () => { + try { + const openedMenusInLocalStorage = localStorage.getItem( + MENU_ITEMS_LOCAL_STORAGE_KEY, + ); + + if (typeof openedMenusInLocalStorage === "string") { + const parsedMenus = tryToJson(openedMenusInLocalStorage) as string[]; + + if (Array.isArray(parsedMenus) && parsedMenus.length > 0) { + return parsedMenus; + } + } + return defaultOpenedMenus; + } catch { + return Promise.reject("Error while getting opened menus "); + } +}; diff --git a/ui-next/src/shared/PersistableSidebar/state/types.ts b/ui-next/src/shared/PersistableSidebar/state/types.ts new file mode 100644 index 0000000000..9e3e09db25 --- /dev/null +++ b/ui-next/src/shared/PersistableSidebar/state/types.ts @@ -0,0 +1,68 @@ +export interface SidebarMachineContext { + openedMenus: string[]; + stashedMenus: string[]; + isBannerOpen: boolean; + isSearchModalOpen: boolean; +} + +export enum PersistableSidebarStates { + INIT = "INIT", + EXPANDED = "EXPANDED", + COLLAPSED = "COLLAPSED", +} + +export enum PersistableSidebarEventTypes { + TOGGLE_BANNER_EVENT = "TOGGLE_BANNER_EVENT", + TOGGLE_SEARCH_MODAL_EVENT = "TOGGLE_SEARCH_MODAL_EVENT", + COLLAPSE_SIDEBAR_EVENT = "COLLAPSE_SIDEBAR_EVENT", + EXPAND_SIDEBAR_EVENT = "EXPAND_SIDEBAR_EVENT", + ADD_MENU_EVENT = "ADD_MENU_EVENT", + REMOVE_MENU_EVENT = "REMOVE_MENU_EVENT", + ADD_MENUS_EVENT = "ADD_MENUS_EVENT", +} + +export type ToggleBannerEventType = { + type: PersistableSidebarEventTypes.TOGGLE_BANNER_EVENT; + data: { + val: boolean; + }; +}; + +export type ToggleSearchModalEventType = { + type: PersistableSidebarEventTypes.TOGGLE_SEARCH_MODAL_EVENT; + data: { + val: boolean; + }; +}; + +export type CollapseSidebarEventType = { + type: PersistableSidebarEventTypes.COLLAPSE_SIDEBAR_EVENT; +}; + +export type ExpandSidebarEventType = { + type: PersistableSidebarEventTypes.EXPAND_SIDEBAR_EVENT; +}; + +export type AddMenuEventType = { + type: PersistableSidebarEventTypes.ADD_MENU_EVENT; + data: { id: string }; +}; + +export type RemoveMenuEventType = { + type: PersistableSidebarEventTypes.REMOVE_MENU_EVENT; + data: { id: string }; +}; + +export type AddMenusEventType = { + type: PersistableSidebarEventTypes.ADD_MENUS_EVENT; + data: { items: string[] }; +}; + +export type PersistableSidebarEvent = + | ToggleBannerEventType + | ToggleSearchModalEventType + | CollapseSidebarEventType + | ExpandSidebarEventType + | AddMenuEventType + | RemoveMenuEventType + | AddMenusEventType; diff --git a/ui-next/src/shared/SectionContainer.tsx b/ui-next/src/shared/SectionContainer.tsx new file mode 100644 index 0000000000..9304b41778 --- /dev/null +++ b/ui-next/src/shared/SectionContainer.tsx @@ -0,0 +1,52 @@ +// WARNING: +// Do not edit this file. Check its twin in the orkes-saas repo. +import { Box } from "@mui/material"; +import { ReactNode } from "react"; + +import { FeatureDisabledWrapper } from "components/FeatureDisabledWrapper"; +import { HEADER_Z_INDEX } from "utils/constants/common"; + +export type SectionContainerProps = { + children?: ReactNode; + header?: ReactNode; + featureDisabledCustomComponent?: ReactNode; +}; + +const SectionContainer = ({ + children, + header = null, + featureDisabledCustomComponent = null, +}: SectionContainerProps) => { + return ( + + {header && ( + ({ + position: "sticky", + top: 0, + zIndex: HEADER_Z_INDEX, + backgroundColor: theme.palette.customBackground.main, + })} + > + {header} + + )} + + + {children} + + + + ); +}; + +export default SectionContainer; diff --git a/ui-next/src/shared/SectionHeader.tsx b/ui-next/src/shared/SectionHeader.tsx new file mode 100644 index 0000000000..d7dbfb434a --- /dev/null +++ b/ui-next/src/shared/SectionHeader.tsx @@ -0,0 +1,92 @@ +import { Box, useMediaQuery } from "@mui/material"; +import { Theme } from "@mui/material/styles"; +import ButtonLinks from "components/v1/layout/header/ButtonLinks"; +import { SidebarContext } from "components/Sidebar/context/SidebarContext"; +import { ReactNode, useContext } from "react"; +import { useLocation } from "react-router"; +import { checkPathFlag } from "utils/checkPathFlag"; + +interface SectionHeaderProps { + title: string; + actions?: ReactNode; + // This should be removed once we + // move the top bar to the shared folder + // and use the same layout in all apps + _deprecate_marginTop?: number; + marginRightForAction?: number; +} +const SIDEBAR_OPEN_BREAKPOINT = 800; +const SIDEBAR_CLOSED_BREAKPOINT = 491; + +const SectionHeader = ({ + title, + actions = null, + _deprecate_marginTop = 65, + marginRightForAction = 0, +}: SectionHeaderProps) => { + const { pathname } = useLocation(); + const { open: isSideBarOpen } = useContext(SidebarContext); + const featureFlagEnabled = checkPathFlag(pathname); + const isValidWidth = useMediaQuery((theme: Theme) => + theme.breakpoints.down( + isSideBarOpen ? SIDEBAR_OPEN_BREAKPOINT : SIDEBAR_CLOSED_BREAKPOINT, + ), + ); + return ( + + + {title} + + + + {actions != null ? actions : null} + + + ); +}; + +export default SectionHeader; diff --git a/ui-next/src/shared/SectionHeaderActions.tsx b/ui-next/src/shared/SectionHeaderActions.tsx new file mode 100644 index 0000000000..7d252267a9 --- /dev/null +++ b/ui-next/src/shared/SectionHeaderActions.tsx @@ -0,0 +1,68 @@ +import { Box } from "@mui/material"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import { Theme } from "@mui/material/styles"; +import Button, { MuiButtonProps } from "components/MuiButton"; +import { useLocation } from "react-router"; +import { checkPathFlag } from "utils/checkPathFlag"; +import { Fragment, ReactNode } from "react"; +import { useAuth } from "./auth"; + +interface IActionButton extends MuiButtonProps { + label?: string; + customButtonElement?: ReactNode; +} +const VALID_WIDTH_BREAKPOINT = 491; + +const SectionHeaderActions = ({ buttons }: { buttons: IActionButton[] }) => { + const { pathname } = useLocation(); + const featureFlagEnabled = checkPathFlag(pathname); + const { isTrialExpired } = useAuth(); + // Checking responsive width + const isValidWidth = useMediaQuery((theme: Theme) => + theme.breakpoints.down(VALID_WIDTH_BREAKPOINT), + ); + + const renderButtons = () => + buttons.map( + ( + { + onClick, + color, + label, + disabled, + customButtonElement, + ...restProps + }: IActionButton, + index: number, + ) => ( + + {customButtonElement ? ( + customButtonElement + ) : ( + + )} + + ), + ); + + return ( + + {renderButtons()} + + ); +}; + +export default SectionHeaderActions; diff --git a/ui-next/src/shared/SideAndTopBarsLayout.tsx b/ui-next/src/shared/SideAndTopBarsLayout.tsx new file mode 100644 index 0000000000..437131b048 --- /dev/null +++ b/ui-next/src/shared/SideAndTopBarsLayout.tsx @@ -0,0 +1,22 @@ +/** + * SideAndTopBarsLayout + * + * Selects the layout component from the plugin registry. + * - Enterprise build: uses AgentLayout (registered by the `additional` plugin) + * - OSS build: falls back to BaseLayout (no AI agent dependencies) + */ + +import { ReactNode } from "react"; +import { pluginRegistry } from "plugins/registry"; +import { BaseLayout } from "shared/BaseLayout"; + +type Props = { + children: ReactNode; +}; + +const SideAndTopBarsLayout = ({ children }: Props) => { + const Layout = pluginRegistry.getAppLayout() ?? BaseLayout; + return {children}; +}; + +export default SideAndTopBarsLayout; diff --git a/ui-next/src/shared/UserSettingsContext.ts b/ui-next/src/shared/UserSettingsContext.ts new file mode 100644 index 0000000000..e1597f92f0 --- /dev/null +++ b/ui-next/src/shared/UserSettingsContext.ts @@ -0,0 +1,11 @@ +import { createContext } from "react"; +import { InterpreterFrom } from "xstate"; +import { userSettingsMachine } from "./state/userSettingsMachine"; + +export interface UserSettingsContextValue { + userSettingsService: InterpreterFrom; +} + +export const UserSettingsContext = createContext< + UserSettingsContextValue | undefined +>(undefined); diff --git a/ui-next/src/shared/UserSettingsProvider.tsx b/ui-next/src/shared/UserSettingsProvider.tsx new file mode 100644 index 0000000000..11113b0927 --- /dev/null +++ b/ui-next/src/shared/UserSettingsProvider.tsx @@ -0,0 +1,22 @@ +import { useInterpret } from "@xstate/react"; +import { FunctionComponent, ReactNode, useMemo } from "react"; +import { userSettingsMachine } from "./state/userSettingsMachine"; +import { UserSettingsContext } from "./UserSettingsContext"; + +interface UserSettingsProviderProps { + children: ReactNode; +} + +export const UserSettingsProvider: FunctionComponent< + UserSettingsProviderProps +> = ({ children }) => { + const service = useInterpret(userSettingsMachine); + + const value = useMemo(() => ({ userSettingsService: service }), [service]); + + return ( + + {children} + + ); +}; diff --git a/ui-next/src/shared/ZoomControlsButton.tsx b/ui-next/src/shared/ZoomControlsButton.tsx new file mode 100644 index 0000000000..79c1408182 --- /dev/null +++ b/ui-next/src/shared/ZoomControlsButton.tsx @@ -0,0 +1,44 @@ +import { Tooltip } from "@mui/material"; +import IconButton, { MuiIconButtonProps } from "components/MuiIconButton"; +import { forwardRef, useContext } from "react"; +import { ColorModeContext } from "theme/material/ColorModeContext"; +import { colors } from "theme/tokens/variables"; + +type ZoomControlsButtonProps = MuiIconButtonProps & { + disabled?: boolean; + tooltip?: string; +}; + +export const ZoomControlsButton = forwardRef< + HTMLButtonElement, + ZoomControlsButtonProps +>(({ style, children, tooltip = "", ...props }, ref) => { + const { mode } = useContext(ColorModeContext); + const darkMode = mode === "dark"; + + return ( + + + {children} + + + ); +}); diff --git a/ui-next/src/shared/agent/agentAtomsStore.ts b/ui-next/src/shared/agent/agentAtomsStore.ts new file mode 100644 index 0000000000..6cd5be1b9b --- /dev/null +++ b/ui-next/src/shared/agent/agentAtomsStore.ts @@ -0,0 +1,140 @@ +// Temporary store for agent state, +// will be replaced with a more permanent store after migrating to the latest XState. + +import { atom } from "jotai"; +import { WorkflowDefinitionEvents } from "pages/definition/state"; +import { ActorRef } from "xstate"; +import { + AgentContentTab, + AgentDisplayMode, + Conversation, +} from "components/agent/agent-types"; +import { WorkflowDef } from "types/WorkflowDef"; +import { atomWithStorage } from "jotai/utils"; +import { CreateAndDisplayApplicationEvents } from "shared/createAndDisplayApplication/state/types"; + +export const setDefinitionServiceAtom = atom( + null, + (get, set, service: ActorRef) => { + set(definitionActorAtom, service); + }, +); + +export const definitionActorAtom = + atom | null>(null); + +export const createAndDisplayApplicationActorAtom = + atom | null>(null); + +export const agentDisplayModeAtom = atomWithStorage( + "agentDisplayMode", + AgentDisplayMode.FLOATING_MINIMIZED, +); + +export const agentWidthAtom = atomWithStorage("agentWidth", 400); + +export const messagesAtom = atom<[]>([]); + +export const sessionIdAtom = atom(null); + +export const isConnectedAtom = atom(true); + +export const isStreamingAtom = atom(false); + +export const workflowNameAtom = atom(null); + +export const currentWorkflowAtom = atom | null>(null); + +export const errorAtom = atom(null); + +export const tokenUsageAtom = atom(null); + +/** + * Current AI context based on the active page/route. + * Determines which prompt and tools are available to the AI. + * + * Possible values: + * - "general" - Q&A and help (default) + * - "workflow_builder" - Workflow building page + * - "workflow_search" - Workflow search/list page + * - "execution_search" - Execution search/list page + * - "execution_details" - Execution details page + * - "task_definitions" - Task definitions page + * - "integrations" - Integrations page + */ +export const aiContextAtom = atom("general"); + +/** + * Additional context-specific data to send with AI requests. + * For example: execution ID when on execution details page. + */ +export const aiContextDataAtom = atom>({}); + +/** + * The current tab of the agent content. + * Possible values: + * - AgentContentTab.CHAT - Chat tab (default) + * - AgentContentTab.CONVERSATIONS - Conversations tab + */ +export const agentContentTabAtom = atom(AgentContentTab.CHAT); + +/** + * The conversations list. + * Populated dynamically from the backend API. + */ +export const conversationsAtom = atom([]); + +/** + * Whether the agent has been used for the first time. + * Used to show the button highlight. + */ +export const agentFirstUseAtom = atomWithStorage( + "agentFirstUse", + false, +); + +export interface CodeAttachment { + id: string; + filename: string; + messageId: string; +} + +export const codeAttachmentsAtom = atom([]); + +export const addCodeAttachmentAtom = atom( + null, + (get, set, attachment: CodeAttachment) => { + const currentAttachments = get(codeAttachmentsAtom); + set(codeAttachmentsAtom, [...currentAttachments, attachment]); + }, +); + +export const removeCodeAttachmentAtom = atom( + null, + (get, set, attachmentId: string) => { + const currentAttachments = get(codeAttachmentsAtom); + set( + codeAttachmentsAtom, + currentAttachments.filter((a) => a.id !== attachmentId), + ); + }, +); + +export const clearCodeAttachmentsAtom = atom(null, (get, set) => { + set(codeAttachmentsAtom, []); +}); + +/** + * Integration configuration request from AI chat. + * When set, shows the integration dialog and disables the chat. + */ +export interface IntegrationConfigurationRequest { + integrationType: string; + suggestedName: string; + reason?: string; + prefilledValues?: Record; + resumeContext?: string; +} + +export const integrationConfigurationRequestAtom = + atom(null); diff --git a/ui-next/src/shared/agent/useAiContext.ts b/ui-next/src/shared/agent/useAiContext.ts new file mode 100644 index 0000000000..49b9b8cead --- /dev/null +++ b/ui-next/src/shared/agent/useAiContext.ts @@ -0,0 +1,75 @@ +import { useEffect } from "react"; +import { useLocation } from "react-router"; +import { useSetAtom } from "jotai"; +import { aiContextAtom, aiContextDataAtom } from "./agentAtomsStore"; + +/** + * Hook that automatically detects the current page context and updates the AI context atom. + * This determines which AI prompt and tools are available based on the active route. + * + * Usage: Call this hook in a global layout component (like SideAndTopBarsLayout) + * + * Context Mapping: + * - /workflow/[id]/edit -> "workflow_builder" + * - /workflows, /workflowDef -> "workflow_search" + * - /execution/[id] -> "execution_details" + * - /taskDef -> "task_definitions" + * - /integrations -> "integrations" + * - Everything else -> "general" + */ +export const useAiContext = () => { + const location = useLocation(); + const setAiContext = useSetAtom(aiContextAtom); + const setAiContextData = useSetAtom(aiContextDataAtom); + + useEffect(() => { + const path = location.pathname; + let newContext = "general"; + const contextData: Record = {}; + + // Workflow builder (editing a workflow OR creating new) + // /workflowDef/ is the builder screen + // /newWorkflowDef is creating a new workflow + if ( + path.includes("/workflowDef/") || + path.includes("/newWorkflowDef") || + (path.includes("/workflow/") && path.includes("/edit")) + ) { + newContext = "workflow_builder"; + } + // Workflow search/list - /workflows (plural, no specific workflow) + else if (path === "/workflows" || path.startsWith("/workflows?")) { + newContext = "workflow_search"; + } + // Execution search/list - /executions (plural) + else if (path === "/executions" || path.startsWith("/executions?")) { + newContext = "execution_search"; + } + // Execution details - /execution/ (singular) + else if (path.includes("/execution/") && path.split("/").length >= 3) { + newContext = "execution_details"; + // Extract execution ID from URL + const parts = path.split("/"); + const executionIndex = parts.indexOf("execution"); + if (executionIndex >= 0 && parts[executionIndex + 1]) { + contextData.executionId = parts[executionIndex + 1]; + console.log(`📋 Execution ID: ${contextData.executionId}`); + } + } + // Task definitions + else if (path.includes("/taskDef")) { + newContext = "task_definitions"; + } + // Integrations + else if (path.includes("/integrations")) { + newContext = "integrations"; + } + // Default to general context + else { + newContext = "general"; + } + + setAiContext(newContext); + setAiContextData(contextData); + }, [location.pathname, setAiContext, setAiContextData]); +}; diff --git a/ui-next/src/shared/auth/AuthProvider.tsx b/ui-next/src/shared/auth/AuthProvider.tsx new file mode 100644 index 0000000000..2379c72709 --- /dev/null +++ b/ui-next/src/shared/auth/AuthProvider.tsx @@ -0,0 +1,74 @@ +/** + * Auth Provider Selection + * + * This module selects the appropriate authentication provider based on configuration. + * The NoAuthProvider is the default (OSS mode). + * + * Enterprise auth providers (Auth0, Okta, OIDC) can be registered via the plugin system. + * When ACCESS_MANAGEMENT is enabled and a provider is registered, it will be used. + */ +import { ComponentType, ReactNode, useMemo } from "react"; +import { pluginRegistry } from "plugins/registry"; +import { featureFlags, FEATURES } from "utils/flags"; +import { logger } from "utils/logger"; +import { NoAuthProvider } from "./NoAuthProvider"; + +// Define the common interface for all auth providers +interface AuthProviderProps { + children: ReactNode; +} + +type AuthProviderType = ComponentType; + +/** + * Select the appropriate auth provider based on configuration. + * + * If ACCESS_MANAGEMENT is enabled: + * - Check plugin registry for registered auth providers (enterprise) + * - Fall back to NoAuthProvider if no matching provider found + * + * If ACCESS_MANAGEMENT is disabled: + * - Use NoAuthProvider (no authentication required) + */ +function selectAuthProvider(): AuthProviderType { + const accessMgmtEnabled = featureFlags.isEnabled(FEATURES.ACCESS_MANAGEMENT); + + if (!accessMgmtEnabled) { + return NoAuthProvider; + } + + const authProviderType = (window as { authConfig?: { type?: string } }) + .authConfig?.type; + + if (!authProviderType) { + return NoAuthProvider; + } + + // Check plugin registry for the auth provider (registered by enterprise) + const pluginAuthProvider = pluginRegistry.getAuthProvider(authProviderType); + + if (pluginAuthProvider) { + logger.log(`Using ${authProviderType} as Auth Provider (from plugin)`); + return pluginAuthProvider; + } + + // No matching provider found + logger.warn( + `Auth provider type "${authProviderType}" not found in plugin registry. ` + + `Falling back to NoAuthProvider.`, + ); + return NoAuthProvider; +} + +/** + * AuthProvider component that lazily selects the provider at render time. + * This allows enterprise plugins to register their auth providers before selection. + */ +function AuthProvider({ children }: AuthProviderProps) { + // Select provider at render time (after plugins are registered) + const SelectedProvider = useMemo(() => selectAuthProvider(), []); + + return {children}; +} + +export { AuthProvider }; diff --git a/ui-next/src/shared/auth/NoAuthProvider/NoAuthProvider.tsx b/ui-next/src/shared/auth/NoAuthProvider/NoAuthProvider.tsx new file mode 100644 index 0000000000..e1671d0d9a --- /dev/null +++ b/ui-next/src/shared/auth/NoAuthProvider/NoAuthProvider.tsx @@ -0,0 +1,39 @@ +/** + * No-auth provider for OSS mode. + * Provides a minimal auth context (stub authState + sidebar machine service). + */ +import { useInterpret } from "@xstate/react"; +import React, { FunctionComponent } from "react"; +import { authProviderMachine, SupportedProviders } from "../../state"; +import { AuthContext } from "../context"; +import { defaultAuthState } from "../types"; + +interface NoAuthProviderProps { + children: React.ReactNode; +} + +export const NoAuthProvider: FunctionComponent = ({ + children, +}) => { + const service = useInterpret(authProviderMachine, { + ...(process.env.NODE_ENV === "development" ? { devTools: true } : {}), + context: { + error: undefined, + providerUser: undefined, + provider: SupportedProviders.NO_USER, + isTrialExpired: false, + isAnnouncementBannerDismissed: false, + }, + }); + + const authState = React.useMemo( + () => ({ ...defaultAuthState, authService: service }), + [service], + ); + + return ( + + {children} + + ); +}; diff --git a/ui-next/src/shared/auth/NoAuthProvider/index.ts b/ui-next/src/shared/auth/NoAuthProvider/index.ts new file mode 100644 index 0000000000..7f8ef5c7af --- /dev/null +++ b/ui-next/src/shared/auth/NoAuthProvider/index.ts @@ -0,0 +1 @@ +export * from "./NoAuthProvider"; diff --git a/ui-next/src/shared/auth/constants.ts b/ui-next/src/shared/auth/constants.ts new file mode 100644 index 0000000000..9d86258800 --- /dev/null +++ b/ui-next/src/shared/auth/constants.ts @@ -0,0 +1,3 @@ +// localStorage keys for token persistence +// Note: Refresh tokens are now stored in memory only (not localStorage) for security +export const TOKEN_EXPIRES_AT_KEY = "token-expires-at"; // Used by both OIDC and Auth0 diff --git a/ui-next/src/shared/auth/context.ts b/ui-next/src/shared/auth/context.ts new file mode 100644 index 0000000000..7557120236 --- /dev/null +++ b/ui-next/src/shared/auth/context.ts @@ -0,0 +1,23 @@ +/** + * Auth context for providing the auth state machine service and/or full auth state. + * Used by useAuth() to access the current auth state. + * + * In OSS mode, NoAuthProvider sets both authService and authState (stub). + * Enterprise (e.g. orkes) can provide authState so conductor-ui's useAuth() and + * shared components (e.g. UserInfo) work without a custom footer. + */ +import { createContext } from "react"; +import { ActorRef } from "xstate"; +import { AuthProviderMachineEvents } from "../state/types"; +import type { AuthState } from "./types"; + +interface AuthContextProps { + authService?: ActorRef; + /** When set (e.g. by enterprise), useAuth() returns this; otherwise stub + authService. */ + authState?: AuthState; +} + +export const AuthContext = createContext({ + authService: undefined, + authState: undefined, +}); diff --git a/ui-next/src/shared/auth/index.ts b/ui-next/src/shared/auth/index.ts new file mode 100644 index 0000000000..dabdde667f --- /dev/null +++ b/ui-next/src/shared/auth/index.ts @@ -0,0 +1 @@ +export * from "./useAuth"; diff --git a/ui-next/src/shared/auth/silentRefresh.ts b/ui-next/src/shared/auth/silentRefresh.ts new file mode 100644 index 0000000000..548926f5ad --- /dev/null +++ b/ui-next/src/shared/auth/silentRefresh.ts @@ -0,0 +1,26 @@ +/** + * Silent token refresh stubs for OSS mode (no authentication). + * Full implementation has been moved to the enterprise package. + * + * All functions are no-ops since OSS mode does not use authentication. + */ + +/** Reset the refresh failure flag. In OSS, this is a no-op. */ +export function resetRefreshFailureFlag(): void { + // No-op in OSS mode +} + +/** Check if refresh has permanently failed. In OSS, always returns false. */ +export function hasRefreshPermanentlyFailed(): boolean { + return false; +} + +/** + * Silently refresh the access token. + * In OSS, always returns false since there's no authentication. + */ +export async function silentlyRefreshToken( + _oidcConfig?: unknown, +): Promise { + return false; +} diff --git a/ui-next/src/shared/auth/tokenManagerJotai.ts b/ui-next/src/shared/auth/tokenManagerJotai.ts new file mode 100644 index 0000000000..02d028eb51 --- /dev/null +++ b/ui-next/src/shared/auth/tokenManagerJotai.ts @@ -0,0 +1,110 @@ +/** + * Token management stubs for OSS mode (no authentication). + * Full implementation has been moved to the enterprise package. + * + * All functions are no-ops or return null/empty values since + * OSS mode does not use authentication tokens. + */ +import { AuthHeaders } from "types"; + +export interface TokenData { + accessToken: string; + idToken?: string; + refreshToken?: string; + expiresAt?: number; +} + +export interface PartialTokenData { + accessToken?: string; + idToken?: string; + refreshToken?: string; + expiresAt?: number; +} + +/** Subscribe to token changes. In OSS, this is a no-op. */ +export function subscribe(_listener: () => void): () => void { + return () => {}; +} + +/** Store token data. In OSS, this is a no-op. */ +export function setTokenData( + _tokenData: TokenData | PartialTokenData, + _useIdToken?: boolean, +): void { + // No-op in OSS mode +} + +/** Get token data. In OSS, always returns null. */ +export function getTokenData(): TokenData | null { + return null; +} + +/** Get complete token data. In OSS, always returns nulls. */ +export function getCompleteTokenData(): { + accessToken: string | null; + idToken: string | null; + refreshToken: string | null; + expiresAt: number | null; +} { + return { + accessToken: null, + idToken: null, + refreshToken: null, + expiresAt: null, + }; +} + +/** Get access token. In OSS, always returns null. */ +export function getAccessToken(): string | null { + return null; +} + +/** Get refresh token. In OSS, always returns null. */ +export function getRefreshToken(): string | null { + return null; +} + +/** Get auth headers. In OSS, always returns empty object. */ +export function getAuthHeaders(): AuthHeaders { + return {}; +} + +/** Store auth headers. In OSS, this is a no-op. */ +export function setAuthHeaders(_authHeaders: AuthHeaders): void { + // No-op in OSS mode +} + +/** Get stored auth headers. In OSS, always returns empty object. */ +export function getStoredAuthHeaders(): AuthHeaders { + return {}; +} + +/** Clear all tokens. In OSS, this is a no-op. */ +export function clear(): void { + // No-op in OSS mode +} + +/** Check if token is expired. In OSS, always returns false. */ +export function isTokenExpired(): boolean { + return false; +} + +/** Check if token is malformed. In OSS, always returns true (no token). */ +export function isTokenMalformed(_token: string | null): boolean { + return true; +} + +/** Check if token should be refreshed. In OSS, always returns false. */ +export function shouldRefreshToken(): boolean { + return false; +} + +/** Check if token can be refreshed. In OSS, always returns false. */ +export function canRefreshToken(): boolean { + return false; +} + +/** Get current auth headers. In OSS, always returns empty object. */ +export function getCurrentAuthHeaders(): AuthHeaders { + return {}; +} diff --git a/ui-next/src/shared/auth/types.ts b/ui-next/src/shared/auth/types.ts new file mode 100644 index 0000000000..7fe7b22b3c --- /dev/null +++ b/ui-next/src/shared/auth/types.ts @@ -0,0 +1,51 @@ +/** + * Auth state returned by useAuth(). + * Default is no auth (defaultAuthState). OSS or custom providers can set + * authState in AuthContext to enable auth without the enterprise package. + */ +import { SupportedProviders } from "../state/types"; +import { User } from "types/User"; + +export interface AuthState { + user: unknown; + isAuthenticated: boolean; + isTrialExpired: boolean; + trialExpiryDate: number | Date | undefined; + isAnnouncementBannerDismissed: boolean; + provider: SupportedProviders; + conductorUser: User | undefined; + oidcConfig: unknown; + authService: unknown; + fetchingUserInformation: boolean; + logOut: () => void; + solveExpireToken: () => void; + setToken: (token: string) => void; + redirectToAuthorizationEndpoint: (currentPath: string) => void; + fetchOidcTokenWithCode: (code: string, stateParam: string) => void; + dismissAnnouncementBanner: () => void; +} + +const noop = () => {}; +const noopSetToken = (_token: string) => {}; +const noopRedirect = (_currentPath: string) => {}; +const noopFetchOidc = (_code: string, _stateParam: string) => {}; + +/** Default when no auth is configured. OSS can add auth by providing a custom auth provider that sets authState in context. */ +export const defaultAuthState: AuthState = { + user: undefined, + isAuthenticated: false, + isTrialExpired: false, + trialExpiryDate: undefined, + isAnnouncementBannerDismissed: false, + provider: SupportedProviders.NO_USER, + conductorUser: undefined, + oidcConfig: undefined, + authService: undefined, + fetchingUserInformation: false, + logOut: noop, + solveExpireToken: noop, + setToken: noopSetToken, + redirectToAuthorizationEndpoint: noopRedirect, + fetchOidcTokenWithCode: noopFetchOidc, + dismissAnnouncementBanner: noop, +}; diff --git a/ui-next/src/shared/auth/useAuth.ts b/ui-next/src/shared/auth/useAuth.ts new file mode 100644 index 0000000000..9c238eb0e5 --- /dev/null +++ b/ui-next/src/shared/auth/useAuth.ts @@ -0,0 +1,14 @@ +/** + * Auth hook. Reads from AuthContext: when authState is provided (e.g. by enterprise), + * returns it; otherwise returns stub values plus authService from context. + * Shared components (UserInfo, Sidebar, etc.) use this so OSS and enterprise share one contract. + */ +import { useContext } from "react"; +import { AuthContext } from "./context"; +import { defaultAuthState } from "./types"; + +export const useAuth = () => { + const { authService, authState } = useContext(AuthContext); + if (authState != null) return authState; + return { ...defaultAuthState, authService } as const; +}; diff --git a/ui-next/src/shared/createAndDisplayApplication/MetadataBanner.tsx b/ui-next/src/shared/createAndDisplayApplication/MetadataBanner.tsx new file mode 100644 index 0000000000..b7e2079261 --- /dev/null +++ b/ui-next/src/shared/createAndDisplayApplication/MetadataBanner.tsx @@ -0,0 +1,287 @@ +import { + Box, + IconButton, + Paper, + Typography, + Button, + Alert, + Fade, +} from "@mui/material"; +import ContentCopyIcon from "@mui/icons-material/ContentCopy"; +import KeyIcon from "@mui/icons-material/Key"; +import CloseIcon from "@mui/icons-material/Close"; +import { ReactElement, useState } from "react"; +import { ActorRef } from "xstate"; +import { useSelector } from "@xstate/react"; +import { + AccessKey, + CreateAndDisplayApplicationEvents, + CreateAndDisplayApplicationMachineEventTypes, +} from "./state/types"; +import { LibraryBooks } from "@mui/icons-material"; +import { logrocketTrackIfEnabled } from "utils/logrocket"; + +interface MetadataBannerStatelessProps { + installScript?: string; + readme?: string; + isDisplayKeys: boolean; + isErrorCreatingApp?: boolean; + applicationAccessKey: AccessKey; // Using 'any' here since the type isn't clear from the original code + onCopy: () => void; + onClose?: () => void; + KeysDisplayerComponent: (props: { + onClose: () => void; + accessKeys: AccessKey; + }) => ReactElement; + onGetAccessKey: () => void; + onRecreateKeys: () => void; + onCloseKeysDialog: () => void; + errorCreatingAppMessage?: string; +} + +export const MetadataBannerStateless = ({ + installScript, + isDisplayKeys, + applicationAccessKey, + readme, + onCopy, + onClose, + onGetAccessKey, + onRecreateKeys, + onCloseKeysDialog, + KeysDisplayerComponent, + isErrorCreatingApp, + errorCreatingAppMessage, +}: MetadataBannerStatelessProps) => { + return installScript == null ? null : ( + + {onClose != null ? ( + + + + ) : null} + + Local Workers Needed + + + + 1. You need to run local workers to try out this workflow. Copy/Paste + this Command into your Terminal. + + + + + {installScript || "$"} + + + + + + + 2. When prompted, enter your Access ID + Key from the button below. + + + {applicationAccessKey == null ? ( + + ) : ( + + )} + {readme ? ( + + ) : null} + + {isDisplayKeys && ( + + )} + +
    + + {errorCreatingAppMessage} + +
    +
    +
    + ); +}; + +interface MetadataBannerProps { + createAndDisplayAppActor: ActorRef; + KeysDisplayerComponent: (props: { + onClose: () => void; + accessKeys: AccessKey; + }) => ReactElement; + onClose?: () => void; + installScript?: string; + readme?: string; +} + +export const MetadataBanner = ({ + createAndDisplayAppActor: metadataEditorActor, + onClose, + installScript, + readme, + KeysDisplayerComponent, +}: MetadataBannerProps) => { + const isDisplayKeys = useSelector(metadataEditorActor, (state) => + state.hasTag("displayKeys"), + ); + const isErrorCreatingApp = useSelector(metadataEditorActor, (state) => + state.hasTag("displayError"), + ); + const errorCreatingAppMessage = useSelector( + metadataEditorActor, + (state) => state.context.errorCreatingAppMessage, + ); + const applicationAccessKey = useSelector( + metadataEditorActor, + (state) => state.context.applicationAccessKey, + ); + + const [, setCopied] = useState(false); + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(installScript || ""); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + logrocketTrackIfEnabled("user_copy_install_script", { installScript }); + } catch (err) { + console.error("Failed to copy text: ", err); + } + }; + + const handleGetAccessKey = () => { + metadataEditorActor.send({ + type: CreateAndDisplayApplicationMachineEventTypes.CREATE_APPLICATION, + }); + + logrocketTrackIfEnabled("user_created_access_key_in_metadata_banner"); + }; + + const handleCloseKeysDialog = () => { + metadataEditorActor.send({ + type: CreateAndDisplayApplicationMachineEventTypes.CLOSE_KEYS_DIALOG, + }); + }; + + const handleRecreateKeys = () => { + metadataEditorActor.send({ + type: CreateAndDisplayApplicationMachineEventTypes.RECREATE_KEYS, + }); + }; + + return ( + + ); +}; diff --git a/ui-next/src/shared/createAndDisplayApplication/state/actions.ts b/ui-next/src/shared/createAndDisplayApplication/state/actions.ts new file mode 100644 index 0000000000..f323948b41 --- /dev/null +++ b/ui-next/src/shared/createAndDisplayApplication/state/actions.ts @@ -0,0 +1,36 @@ +import { assign, DoneInvokeEvent } from "xstate"; +import { CreateAndDisplayApplicationMachineContext } from "./types"; + +export const persistApplicationKeys = assign< + CreateAndDisplayApplicationMachineContext, + DoneInvokeEvent<{ id: string; secret: string }> +>((_context, { data }) => { + return { + applicationAccessKey: { + id: data.id, + secret: data.secret, + }, + }; +}); + +export const persistApplicationId = assign< + CreateAndDisplayApplicationMachineContext, + DoneInvokeEvent<{ id: string }> +>((_context, { data }) => { + return { + applicationId: data.id, + }; +}); + +export const persistError = assign< + CreateAndDisplayApplicationMachineContext, + DoneInvokeEvent<{ message: string }> +>((_context, { data }) => { + return { errorCreatingAppMessage: data.message }; +}); + +export const clearError = assign( + () => { + return { errorCreatingAppMessage: undefined }; + }, +); diff --git a/ui-next/src/shared/createAndDisplayApplication/state/guards.ts b/ui-next/src/shared/createAndDisplayApplication/state/guards.ts new file mode 100644 index 0000000000..dd86ba52dd --- /dev/null +++ b/ui-next/src/shared/createAndDisplayApplication/state/guards.ts @@ -0,0 +1,7 @@ +import { CreateAndDisplayApplicationMachineContext } from "./types"; + +export const isApplicationCreated = ({ + applicationId, +}: CreateAndDisplayApplicationMachineContext) => { + return applicationId !== undefined; +}; diff --git a/ui-next/src/shared/createAndDisplayApplication/state/machine.ts b/ui-next/src/shared/createAndDisplayApplication/state/machine.ts new file mode 100644 index 0000000000..67b68185ad --- /dev/null +++ b/ui-next/src/shared/createAndDisplayApplication/state/machine.ts @@ -0,0 +1,112 @@ +import { createMachine } from "xstate"; +import { + CreateAndDisplayApplicationMachineContext, + CreateAndDisplayApplicationEvents, + CreateAndDisplayApplicationMachineEventTypes, +} from "./types"; +import * as services from "./services"; +import * as actions from "./actions"; +import * as guards from "./guards"; + +export const createAndDisplayApplicationMachine = createMachine< + CreateAndDisplayApplicationMachineContext, + CreateAndDisplayApplicationEvents +>( + { + id: "createAndDisplayApplicationMachine", + predictableActionArguments: true, + context: { + applicationAccessKey: undefined, + }, + initial: "fetchForExistingApplication", + states: { + fetchForExistingApplication: { + invoke: { + src: "checkIfAppExistsAndCompatible", + onDone: [ + { + cond: (_context, { data }) => data.id !== null, + actions: ["persistApplicationId"], + target: "idle", + }, + { + target: "idle", + }, + ], + }, + }, + idle: { + on: { + [CreateAndDisplayApplicationMachineEventTypes.CREATE_APPLICATION]: { + target: "shouldCreateApplication", + }, + [CreateAndDisplayApplicationMachineEventTypes.RECREATE_KEYS]: { + target: "generateKeys", + }, + }, + }, + shouldCreateApplication: { + always: [ + { + cond: "isApplicationCreated", + target: "generateKeys", + }, + { + target: "createApplication", + }, + ], + }, + generateKeys: { + invoke: { + src: "generateKeys", + onDone: { + target: "displayKeys", + actions: ["persistApplicationKeys"], + }, + onError: { + target: "errorCreatingApplication", + actions: ["persistError"], + }, + }, + }, + createApplication: { + invoke: { + src: "createApplicationWithRoles", + onDone: { + target: "generateKeys", + actions: ["persistApplicationId"], + }, + onError: { + target: "errorCreatingApplication", + actions: ["persistError"], + }, + }, + }, + displayKeys: { + tags: ["displayKeys"], + on: { + [CreateAndDisplayApplicationMachineEventTypes.CLOSE_KEYS_DIALOG]: { + target: "idle", + }, + [CreateAndDisplayApplicationMachineEventTypes.RECREATE_KEYS]: { + target: "generateKeys", + }, + }, + }, + errorCreatingApplication: { + tags: ["displayError"], + after: { + 3000: { + target: "idle", + actions: ["clearError"], + }, + }, + }, + }, + }, + { + actions: actions as any, + guards: guards as any, + services: services as any, + }, +); diff --git a/ui-next/src/shared/createAndDisplayApplication/state/services.ts b/ui-next/src/shared/createAndDisplayApplication/state/services.ts new file mode 100644 index 0000000000..28f319489c --- /dev/null +++ b/ui-next/src/shared/createAndDisplayApplication/state/services.ts @@ -0,0 +1,132 @@ +import { fetchWithContext } from "plugins/fetch"; +import { CreateAndDisplayApplicationMachineContext } from "./types"; +import { getErrorMessage } from "utils/utils"; +import { AccessRole, User } from "types/User"; +// const fetchContext = fetchContextNonHook(); + +export const createApplication = async ( + context: CreateAndDisplayApplicationMachineContext, +) => { + const { authHeaders, applicationName } = context; + try { + return await fetchWithContext( + "/applications", + {}, + { + method: "POST", + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + body: JSON.stringify({ name: applicationName }), + }, + ); + } catch (error: any) { + const errorMessage = await getErrorMessage(error); + throw new Error(errorMessage ?? "Failed to create application"); + } +}; + +export const fetchForAppDetails = async ( + context: CreateAndDisplayApplicationMachineContext, +): Promise => { + const { authHeaders, applicationId } = context; + const path = `/users/app:${applicationId}`; + + const appDetails = await fetchWithContext( + path, + {}, + { + method: "GET", + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + }, + ); + return appDetails; +}; + +export const checkIfAppExistsAndCompatible = async ( + context: CreateAndDisplayApplicationMachineContext, +) => { + const { authHeaders, applicationName } = context; + try { + const appList = await fetchWithContext( + "/applications", + {}, + { + method: "GET", + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + }, + ); + + const app = appList.find((app: any) => app.name === applicationName); + + if (app) { + const appUserDetails = await fetchForAppDetails({ + ...context, + applicationId: app.id, + }); + if ( + appUserDetails.roles.find( + (role: AccessRole) => role.name === "UNRESTRICTED_WORKER", + ) + ) { + return { id: app.id }; + } + } + + return { id: null }; + } catch (error: any) { + const errorMessage = await getErrorMessage(error); + throw new Error(errorMessage ?? "Failed to create application"); + } +}; + +export const createApplicationWithRoles = async ( + context: CreateAndDisplayApplicationMachineContext, +) => { + const { authHeaders } = context; + const appCreateResponse = await createApplication(context); + const { id } = appCreateResponse; + + const path = `/applications/${id}/roles/UNRESTRICTED_WORKER`; + + try { + await fetchWithContext( + path, + {}, + { + method: "POST", + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + }, + ); + return appCreateResponse; + } catch (error: any) { + const errorMessage = await getErrorMessage(error); + throw new Error(errorMessage ?? "Failed to create application"); + } +}; + +export const generateKeys = async ( + context: CreateAndDisplayApplicationMachineContext, +) => { + const { authHeaders, applicationId } = context; + const path = `/applications/${applicationId}/accessKeys`; + try { + return await fetchWithContext( + path, + {}, + { method: "POST", headers: { ...authHeaders } }, + ); + } catch { + throw new Error("Failed to generate keys"); + } +}; diff --git a/ui-next/src/shared/createAndDisplayApplication/state/types.ts b/ui-next/src/shared/createAndDisplayApplication/state/types.ts new file mode 100644 index 0000000000..cac00a8a3e --- /dev/null +++ b/ui-next/src/shared/createAndDisplayApplication/state/types.ts @@ -0,0 +1,37 @@ +import { AuthHeaders } from "types/common"; + +export interface AccessKey { + id: string; + secret: string; +} + +export type CreateAndDisplayApplicationMachineContext = { + applicationAccessKey?: AccessKey; + authHeaders?: AuthHeaders; + applicationName?: string; + applicationId?: string; + errorCreatingAppMessage?: string; +}; + +export enum CreateAndDisplayApplicationMachineEventTypes { + CREATE_APPLICATION = "CREATE_APPLICATION", + CLOSE_KEYS_DIALOG = "CLOSE_KEYS_DIALOG", + RECREATE_KEYS = "RECREATE_KEYS", +} + +export type CreateApplicationEvent = { + type: CreateAndDisplayApplicationMachineEventTypes.CREATE_APPLICATION; +}; + +export type CloseKeysDialogEvent = { + type: CreateAndDisplayApplicationMachineEventTypes.CLOSE_KEYS_DIALOG; +}; + +export type RecreateApplicationEvent = { + type: CreateAndDisplayApplicationMachineEventTypes.RECREATE_KEYS; +}; + +export type CreateAndDisplayApplicationEvents = + | CreateApplicationEvent + | CloseKeysDialogEvent + | RecreateApplicationEvent; diff --git a/ui-next/src/shared/editor.ts b/ui-next/src/shared/editor.ts new file mode 100644 index 0000000000..ce412c725b --- /dev/null +++ b/ui-next/src/shared/editor.ts @@ -0,0 +1,11 @@ +import { editor } from "monaco-editor"; + +export type EditorOptions = editor.IStandaloneEditorConstructionOptions; +export type DiffEditorOptions = editor.IDiffEditorConstructionOptions; +export { editor }; + +export const defaultEditorOptions: EditorOptions = { + stickyScroll: { + enabled: false, + }, +}; diff --git a/ui-next/src/shared/icons/FitToFrame.tsx b/ui-next/src/shared/icons/FitToFrame.tsx new file mode 100644 index 0000000000..42e92aa12f --- /dev/null +++ b/ui-next/src/shared/icons/FitToFrame.tsx @@ -0,0 +1,18 @@ +function Icon({ size = "20", color = "#000000" }) { + return ( + + + + ); +} + +export default Icon; diff --git a/ui-next/src/shared/state/index.ts b/ui-next/src/shared/state/index.ts new file mode 100644 index 0000000000..67904640f8 --- /dev/null +++ b/ui-next/src/shared/state/index.ts @@ -0,0 +1,3 @@ +export * from "./machine"; +export * from "./types"; +export * from "./selectors"; diff --git a/ui-next/src/shared/state/machine.ts b/ui-next/src/shared/state/machine.ts new file mode 100644 index 0000000000..f9c000a138 --- /dev/null +++ b/ui-next/src/shared/state/machine.ts @@ -0,0 +1,45 @@ +/** + * Minimal auth state machine for OSS mode (no authentication). + * Full auth implementation has been moved to the enterprise package. + * + * This minimal machine only handles the NO_USER_MANAGEMENT state + * and spawns the sidebar machine for UI state management. + */ +import { createMachine } from "xstate"; +import { + AuthProviderMachineContext, + AuthProviderStates, + SupportedProviders, +} from "./types"; +import { sidebarMachine } from "shared/PersistableSidebar/state/machine"; + +export const authProviderMachine = createMachine({ + id: "authProviderMachine", + predictableActionArguments: true, + initial: AuthProviderStates.UNLOGGED, + context: { + provider: SupportedProviders.NO_USER, + error: undefined, + providerUser: undefined, + isTrialExpired: false, + isAnnouncementBannerDismissed: false, + }, + states: { + [AuthProviderStates.UNLOGGED]: { + initial: AuthProviderStates.NO_USER_MANAGEMENT, + states: { + [AuthProviderStates.NO_USER_MANAGEMENT]: { + initial: AuthProviderStates.SIDEBAR_INIT, + states: { + [AuthProviderStates.SIDEBAR_INIT]: { + invoke: { + src: sidebarMachine, + id: "sidebarMachine", + }, + }, + }, + }, + }, + }, + }, +}); diff --git a/ui-next/src/shared/state/selectors.ts b/ui-next/src/shared/state/selectors.ts new file mode 100644 index 0000000000..3f3dc7be8a --- /dev/null +++ b/ui-next/src/shared/state/selectors.ts @@ -0,0 +1,27 @@ +import { State } from "xstate"; +import { AuthProviderMachineContext, AuthProviderStates } from "./types"; + +export const isAuthenticated = (state: State) => + state.matches(AuthProviderStates.LOGGED_USER); + +export const noUserManagement = (state: State) => + state.matches([ + AuthProviderStates.UNLOGGED, + AuthProviderStates.NO_USER_MANAGEMENT, + ]); + +export const getUserPersistableProfileActor = ( + state: State, +) => state.children["userPersistableProfileMachine"]; + +export const isSidebarInitialized = ( + state: State, +) => + state.matches([ + AuthProviderStates.LOGGED_USER, + AuthProviderStates.SIDEBAR_INIT, + ]) || + state.matches([ + AuthProviderStates.UNLOGGED, + AuthProviderStates.NO_USER_MANAGEMENT, + ]); diff --git a/ui-next/src/shared/state/types.ts b/ui-next/src/shared/state/types.ts new file mode 100644 index 0000000000..6d15ee58b9 --- /dev/null +++ b/ui-next/src/shared/state/types.ts @@ -0,0 +1,76 @@ +/** + * Minimal auth state types for OSS mode (no authentication). + * Full auth implementation has been moved to the enterprise package. + */ + +/** + * Supported auth providers. In OSS, only NO_USER is used. + */ +export enum SupportedProviders { + AUTH_0 = "auth0", + OKTA = "okta", + OIDC = "oidc", + NO_USER = "NO_USER", +} + +/** + * Auth provider states. In OSS, only UNLOGGED, NO_USER_MANAGEMENT, and SIDEBAR_INIT are used. + */ +export enum AuthProviderStates { + UNLOGGED = "UNLOGGED", + NO_USER_MANAGEMENT = "NO_USER_MANAGEMENT", + SIDEBAR_INIT = "SIDEBAR_INIT", + // Keep these for type compatibility with selectors + LOGGED_USER = "LOGGED_USER", + MANAGE_PROVIDER = "MANAGE_PROVIDER", +} + +/** + * Auth machine event types. In OSS, most events are no-ops. + */ +export enum AuthMachineEventTypes { + LOGOUT = "LOGOUT", + SOLVE_EXPIRED_TOKEN = "SOLVE_EXPIRED_TOKEN", + DISMISS_ANNOUNCEMENT_BANNER = "DISMISS_ANNOUNCEMENT_BANNER", + // Keep these for type compatibility + SET_USER_CREDENTIALS = "SET_USER_CREDENTIALS", + SET_PROVIDER_USER = "SET_PROVIDER_USER", + SET_TOKEN = "SET_TOKEN", + FETCH_FOR_USER_INFO = "FETCH_FOR_USER_INFO", + FETCH_TOKEN = "FETCH_TOKEN", + REFRESH_OIDC_TOKEN = "REFRESH_OIDC_TOKEN", + REDIRECT_TO_AUTHORIZATION_ENDPOINT = "REDIRECT_TO_AUTHORIZATION_ENDPOINT", +} + +/** + * Minimal auth provider machine context for OSS mode. + */ +export interface AuthProviderMachineContext { + provider: SupportedProviders; + error?: unknown; + providerUser?: unknown; + conductorUser?: { id: string }; + isTrialExpired: boolean; + trialExpiryDate?: number | Date; + limits?: unknown; + isAnnouncementBannerDismissed: boolean; + oidcConfig?: unknown; +} + +/** + * Auth provider machine events union type. + */ +export type AuthProviderMachineEvents = + | { type: AuthMachineEventTypes.LOGOUT } + | { type: AuthMachineEventTypes.SOLVE_EXPIRED_TOKEN } + | { type: AuthMachineEventTypes.DISMISS_ANNOUNCEMENT_BANNER } + | { type: AuthMachineEventTypes.SET_TOKEN; data: { token: string } } + | { type: AuthMachineEventTypes.SET_PROVIDER_USER; user: unknown } + | { + type: AuthMachineEventTypes.REDIRECT_TO_AUTHORIZATION_ENDPOINT; + currentPath: string; + } + | { + type: AuthMachineEventTypes.FETCH_TOKEN; + data: { code?: string; state?: string }; + }; diff --git a/ui-next/src/shared/state/userSettingsMachine/actions.ts b/ui-next/src/shared/state/userSettingsMachine/actions.ts new file mode 100644 index 0000000000..3800b9527a --- /dev/null +++ b/ui-next/src/shared/state/userSettingsMachine/actions.ts @@ -0,0 +1,53 @@ +import { assign, DoneInvokeEvent } from "xstate"; +import { + UserSettingsMachineContext, + SetFirstWorkflowExecutedEvent, + AddDismissedMessageEvent, + SetDismissAllMessagesEvent, +} from "./types"; + +export const hydrateFromStorage = assign< + UserSettingsMachineContext, + DoneInvokeEvent> +>((context, event) => { + const loadedData = event.data; + return { + firstWorkflowExecuted: + loadedData.firstWorkflowExecuted ?? context.firstWorkflowExecuted, + dismissedMessages: + loadedData.dismissedMessages ?? context.dismissedMessages, + dismissAllMessages: + loadedData.dismissAllMessages ?? context.dismissAllMessages, + isShowingConfettiThisSession: false, + }; +}); + +export const persistFirstWorkflowExecuted = assign< + UserSettingsMachineContext, + SetFirstWorkflowExecutedEvent +>({ + firstWorkflowExecuted: (_, event) => event.value, + isShowingConfettiThisSession: (context, event) => + !context.firstWorkflowExecuted && event.value === true + ? true + : context.isShowingConfettiThisSession, +}); + +export const persistDismissedMessage = assign< + UserSettingsMachineContext, + AddDismissedMessageEvent +>({ + dismissedMessages: (context, event) => { + if (context.dismissedMessages.includes(event.messageId)) { + return context.dismissedMessages; + } + return [...context.dismissedMessages, event.messageId]; + }, +}); + +export const persistDismissAllMessages = assign< + UserSettingsMachineContext, + SetDismissAllMessagesEvent +>({ + dismissAllMessages: (_, event) => event.value, +}); diff --git a/ui-next/src/shared/state/userSettingsMachine/guards.ts b/ui-next/src/shared/state/userSettingsMachine/guards.ts new file mode 100644 index 0000000000..e88bf1c22b --- /dev/null +++ b/ui-next/src/shared/state/userSettingsMachine/guards.ts @@ -0,0 +1,11 @@ +import { + UserSettingsMachineContext, + SetFirstWorkflowExecutedEvent, +} from "./types"; + +export const isFirstWorkflowCompleted = ( + context: UserSettingsMachineContext, + event: SetFirstWorkflowExecutedEvent, +) => { + return !context.firstWorkflowExecuted && event.value === true; +}; diff --git a/ui-next/src/shared/state/userSettingsMachine/index.ts b/ui-next/src/shared/state/userSettingsMachine/index.ts new file mode 100644 index 0000000000..36828922cb --- /dev/null +++ b/ui-next/src/shared/state/userSettingsMachine/index.ts @@ -0,0 +1,2 @@ +export { userSettingsMachine } from "./machine"; +export * from "./types"; diff --git a/ui-next/src/shared/state/userSettingsMachine/machine.ts b/ui-next/src/shared/state/userSettingsMachine/machine.ts new file mode 100644 index 0000000000..6edde926e9 --- /dev/null +++ b/ui-next/src/shared/state/userSettingsMachine/machine.ts @@ -0,0 +1,90 @@ +import { createMachine } from "xstate"; +import { + UserSettingsMachineContext, + UserSettingsStates, + UserSettingsEvents, + UserSettingsEventTypes, +} from "./types"; +import * as actions from "./actions"; +import * as services from "./services"; +import * as guards from "./guards"; + +export const userSettingsMachine = createMachine< + UserSettingsMachineContext, + UserSettingsEvents +>( + { + id: "userSettingsMachine", + predictableActionArguments: true, + initial: UserSettingsStates.INIT, + context: { + firstWorkflowExecuted: false, + dismissedMessages: [], + dismissAllMessages: false, + isShowingConfettiThisSession: false, + }, + states: { + [UserSettingsStates.INIT]: { + always: { + target: UserSettingsStates.LOADING_FROM_STORAGE, + }, + }, + [UserSettingsStates.LOADING_FROM_STORAGE]: { + invoke: { + src: "loadFromLocalStorage", + onDone: { + target: UserSettingsStates.READY, + actions: "hydrateFromStorage", + }, + onError: { + target: UserSettingsStates.READY, + }, + }, + }, + [UserSettingsStates.READY]: { + on: { + [UserSettingsEventTypes.SET_FIRST_WORKFLOW_EXECUTED]: [ + { + cond: "isFirstWorkflowCompleted", + actions: "persistFirstWorkflowExecuted", + target: UserSettingsStates.SHOWING_CONFETTI, + }, + { + actions: "persistFirstWorkflowExecuted", + target: UserSettingsStates.SAVING_TO_STORAGE, + }, + ], + [UserSettingsEventTypes.ADD_DISMISSED_MESSAGE]: { + actions: "persistDismissedMessage", + target: UserSettingsStates.SAVING_TO_STORAGE, + }, + [UserSettingsEventTypes.SET_DISMISS_ALL_MESSAGES]: { + actions: "persistDismissAllMessages", + target: UserSettingsStates.SAVING_TO_STORAGE, + }, + }, + }, + [UserSettingsStates.SHOWING_CONFETTI]: { + always: { + target: UserSettingsStates.SAVING_TO_STORAGE, + }, + }, + [UserSettingsStates.SAVING_TO_STORAGE]: { + invoke: { + src: "saveToLocalStorage", + onDone: { + target: UserSettingsStates.READY, + }, + onError: { + target: UserSettingsStates.READY, + }, + }, + }, + }, + }, + { + actions: actions as any, + services: services as any, + guards: guards as any, + }, +); diff --git a/ui-next/src/shared/state/userSettingsMachine/services.ts b/ui-next/src/shared/state/userSettingsMachine/services.ts new file mode 100644 index 0000000000..f6431957e7 --- /dev/null +++ b/ui-next/src/shared/state/userSettingsMachine/services.ts @@ -0,0 +1,48 @@ +import { tryToJson } from "utils/utils"; +import { logger } from "utils/logger"; +import { UserSettingsMachineContext } from "./types"; + +const USER_SETTINGS_STORAGE_KEY = "userSettings"; + +export const loadFromLocalStorage = async (): Promise< + Partial +> => { + try { + const savedSettings = window.localStorage.getItem( + USER_SETTINGS_STORAGE_KEY, + ); + if (savedSettings) { + const parsedSettings = + tryToJson>(savedSettings); + if (parsedSettings !== undefined) { + return parsedSettings; + } else { + window.localStorage.removeItem(USER_SETTINGS_STORAGE_KEY); + logger.log("Couldn't parse user settings, removing from localStorage."); + } + } + } catch (error) { + logger.error("Error loading user settings from localStorage", error); + } + return {}; +}; + +export const saveToLocalStorage = async ( + context: UserSettingsMachineContext, +) => { + try { + const settingsToSave = { + firstWorkflowExecuted: context.firstWorkflowExecuted, + dismissedMessages: context.dismissedMessages, + dismissAllMessages: context.dismissAllMessages, + }; + window.localStorage.setItem( + USER_SETTINGS_STORAGE_KEY, + JSON.stringify(settingsToSave), + ); + return settingsToSave; + } catch (error) { + logger.error("Error saving user settings to localStorage", error); + throw error; + } +}; diff --git a/ui-next/src/shared/state/userSettingsMachine/types.ts b/ui-next/src/shared/state/userSettingsMachine/types.ts new file mode 100644 index 0000000000..4bc450d2a4 --- /dev/null +++ b/ui-next/src/shared/state/userSettingsMachine/types.ts @@ -0,0 +1,41 @@ +export interface UserSettingsMachineContext { + firstWorkflowExecuted: boolean; + dismissedMessages: string[]; + dismissAllMessages: boolean; + isShowingConfettiThisSession: boolean; +} + +export enum UserSettingsStates { + INIT = "init", + LOADING_FROM_STORAGE = "loadingFromStorage", + READY = "ready", + SHOWING_CONFETTI = "showingConfetti", + CONFETTI_VISIBLE = "confettiVisible", + SAVING_TO_STORAGE = "savingToStorage", +} + +export enum UserSettingsEventTypes { + SET_FIRST_WORKFLOW_EXECUTED = "SET_FIRST_WORKFLOW_EXECUTED", + ADD_DISMISSED_MESSAGE = "ADD_DISMISSED_MESSAGE", + SET_DISMISS_ALL_MESSAGES = "SET_DISMISS_ALL_MESSAGES", +} + +export type SetFirstWorkflowExecutedEvent = { + type: UserSettingsEventTypes.SET_FIRST_WORKFLOW_EXECUTED; + value: boolean; +}; + +export type AddDismissedMessageEvent = { + type: UserSettingsEventTypes.ADD_DISMISSED_MESSAGE; + messageId: string; +}; + +export type SetDismissAllMessagesEvent = { + type: UserSettingsEventTypes.SET_DISMISS_ALL_MESSAGES; + value: boolean; +}; + +export type UserSettingsEvents = + | SetFirstWorkflowExecutedEvent + | AddDismissedMessageEvent + | SetDismissAllMessagesEvent; diff --git a/ui-next/src/shared/styles.ts b/ui-next/src/shared/styles.ts new file mode 100644 index 0000000000..6a3bdb191f --- /dev/null +++ b/ui-next/src/shared/styles.ts @@ -0,0 +1,53 @@ +import { Theme } from "@mui/material"; +import { baseLabelStyle } from "components/v1/theme/styles"; +import { isEmpty as _isEmpty } from "lodash"; +import { greyText2, lightGrey } from "theme/tokens/colors"; + +export const disabledInputStyle = { + "& .MuiOutlinedInput-root.Mui-disabled .MuiOutlinedInput-notchedOutline": { + borderColor: greyText2, + backgroundColor: lightGrey, + }, +}; + +export const dateRangePickerStyle = { + wrapper: { + display: "flex", + }, + input: { + ">div": { width: "100%" }, + ...disabledInputStyle, + }, +}; +export const autocompleteStyle = ({ value }: { value: any }) => ({ + ".MuiTextField-root": { + ".MuiOutlinedInput-root": { + pt: "14px", + pl: "8px", + pb: "8px", + ".MuiAutocomplete-input": { + p: 0, + }, + }, + ".MuiInputLabel-root": { + ...(baseLabelStyle as any), + color: (theme: Theme) => + _isEmpty(value) ? theme.palette.input.text : theme.palette.label.text, + "&.Mui-focused": { + fontWeight: 500, + color: (theme: Theme) => theme.palette.input.focus, + }, + "&.Mui-disabled": { + color: (theme: Theme) => theme.palette.label.disabled, + }, + "&.Mui-error": { + color: (theme: Theme) => theme.palette.input.error, + }, + }, + }, +}); + +export const customButtonStyle = { + color: "#000", + "&:hover": { background: "#0505050a" }, +}; diff --git a/ui-next/src/shared/useSaveProtection.ts b/ui-next/src/shared/useSaveProtection.ts new file mode 100644 index 0000000000..74651058a5 --- /dev/null +++ b/ui-next/src/shared/useSaveProtection.ts @@ -0,0 +1,245 @@ +import { useSelector } from "@xstate/react"; +import { useEffect, useMemo, useRef } from "react"; +import { ActorRef, AnyEventObject, EventObject } from "xstate"; + +export interface SaveProtectionConfig< + TContext, + TEvent extends EventObject = AnyEventObject, +> { + /** + * The actor/machine to monitor for save events and state + */ + actor: ActorRef; + + /** + * Whether there are form changes (false means there are changes) + */ + noFormChanges: boolean; + + /** + * Check if save is in progress. Should return true when saving. + */ + isSaveInProgress: (state: { + context: TContext; + event: TEvent; + matches: (state: string | string[]) => boolean; + hasTag?: (tag: string) => boolean; + }) => boolean; + + /** + * Check for validation errors. Should return true if there are errors. + */ + hasErrors: (state: { + context: TContext; + event: TEvent; + matches: (state: string | string[]) => boolean; + }) => boolean; + + /** + * Optional: Function to detect successful save based on event type. + * Should return true for successful save, false for cancelled, undefined if unknown. + */ + detectSaveSuccessFromEvent?: ( + eventType: string, + state: { + context: TContext; + event: TEvent; + matches: (state: string | string[]) => boolean; + }, + ) => boolean | undefined; + + /** + * Optional: Function to detect successful save based on context changes. + * This is useful for cases where success is detected by comparing previous + * and current context values (e.g., originTaskDefinition changes). + */ + detectSaveSuccessFromContext?: (options: { + currentContext: TContext; + previousContext: TContext | null; + wasSaving: boolean; + isSaving: boolean; + }) => boolean; + + /** + * Function to trigger the save action + */ + handleSaveAction: (actor: ActorRef) => void; +} + +export interface SaveProtectionResult { + /** + * Whether to show the save prompt (true means block navigation) + */ + showPrompt: boolean; + + /** + * Whether the last save was successful (undefined if no save attempted yet) + */ + successfulSave: boolean | undefined; + + /** + * Whether there are validation errors + */ + hasErrors: boolean; + + /** + * Function to trigger the save + */ + handleSave: () => void; +} + +/** + * Generic hook for save protection logic that can be reused across different + * save scenarios (workflows, tasks, etc.) + */ +export function useSaveProtection< + TContext, + TEvent extends EventObject = AnyEventObject, +>(config: SaveProtectionConfig): SaveProtectionResult { + const { + actor, + noFormChanges, + isSaveInProgress: checkIsSaveInProgress, + hasErrors: checkHasErrors, + detectSaveSuccessFromEvent, + detectSaveSuccessFromContext, + handleSaveAction, + } = config; + + // Track the last save result using a ref to persist across renders + const lastSaveResultRef = useRef(undefined); + + // Track previous context for detecting successful saves + const prevContextRef = useRef(null); + const prevIsSavingRef = useRef(false); + + // Get current context + const currentContext = useSelector( + actor, + (state) => state.context as TContext, + ); + + // Get current saving state + const isSaving = useSelector(actor, (state) => + checkIsSaveInProgress({ + context: state.context as TContext, + event: state.event as TEvent, + matches: (statePath) => state.matches(statePath), + hasTag: (tag) => state.hasTag?.(tag) ?? false, + }), + ); + + // Detect successful save from context changes (e.g., originTaskDefinition updated) + useEffect(() => { + if (detectSaveSuccessFromContext) { + const wasSaving = prevIsSavingRef.current; + const isCurrentlySaving = isSaving; + + // Initialize the previous context on first render + if (prevContextRef.current === null) { + prevContextRef.current = currentContext; + } + + // Capture context before we start saving (when transitioning from not saving to saving) + if (!wasSaving && isCurrentlySaving) { + // We're about to start saving, capture the current context as the "before" state + prevContextRef.current = currentContext; + } + + // If we were saving and now we're not, check if save was successful + if (wasSaving && !isCurrentlySaving && prevContextRef.current) { + // Check if context was updated (indicates successful save) + const success = detectSaveSuccessFromContext({ + currentContext, + previousContext: prevContextRef.current, + wasSaving, + isSaving: isCurrentlySaving, + }); + + if (success) { + lastSaveResultRef.current = true; + } + } + + prevIsSavingRef.current = isSaving; + } else { + // If not using context detection, still track saving state + prevIsSavingRef.current = isSaving; + } + }, [isSaving, currentContext, detectSaveSuccessFromContext, actor]); + + // Check for successful save based on event types + const successfulSave = useSelector(actor, (state) => { + const eventType = state.event.type; + + // Check for cancel/success events if configured + if (detectSaveSuccessFromEvent) { + const result = detectSaveSuccessFromEvent(eventType, { + context: state.context as TContext, + event: state.event as TEvent, + matches: (statePath) => state.matches(statePath), + }); + + if (result !== undefined) { + lastSaveResultRef.current = result; + return result; + } + } + + // If we detected a successful save via context, verify there's no error + if (lastSaveResultRef.current === true) { + const hasError = checkHasErrors({ + context: state.context as TContext, + event: state.event as TEvent, + matches: (statePath) => state.matches(statePath), + }); + + if (hasError) { + // If there's an error, it wasn't successful + lastSaveResultRef.current = false; + return false; + } + } + + // Return the last known result + return lastSaveResultRef.current; + }); + + // Check for validation errors + const hasErrors = useSelector(actor, (state) => + checkHasErrors({ + context: state.context as TContext, + event: state.event as TEvent, + matches: (statePath) => state.matches(statePath), + }), + ); + + // Check if save is in progress + const isSaveInProgress = useSelector(actor, (state) => + checkIsSaveInProgress({ + context: state.context as TContext, + event: state.event as TEvent, + matches: (statePath) => state.matches(statePath), + hasTag: (tag) => state.hasTag?.(tag) ?? false, + }), + ); + + // Determine if we should show the prompt + const showPrompt = useMemo( + () => !noFormChanges && !isSaveInProgress, + [isSaveInProgress, noFormChanges], + ); + + // Handle save action + const handleSave = () => { + lastSaveResultRef.current = undefined; + handleSaveAction(actor); + }; + + return { + showPrompt, + successfulSave, + hasErrors, + handleSave, + }; +} diff --git a/ui-next/src/shared/useUserSettings.ts b/ui-next/src/shared/useUserSettings.ts new file mode 100644 index 0000000000..43da443514 --- /dev/null +++ b/ui-next/src/shared/useUserSettings.ts @@ -0,0 +1,30 @@ +import { useSelector } from "@xstate/react"; +import { useContext } from "react"; +import { UserSettingsContext } from "./UserSettingsContext"; +import { UserSettingsMachineContext } from "./state/userSettingsMachine"; + +export const useUserSettings = () => { + const context = useContext(UserSettingsContext); + if (!context) { + throw new Error("useUserSettings must be used within UserSettingsProvider"); + } + + const { userSettingsService } = context; + + const userSettings = useSelector( + userSettingsService, + (state) => state.context as UserSettingsMachineContext, + ); + + const isShowingConfetti = useSelector( + userSettingsService, + (state) => state.context.isShowingConfettiThisSession, + ); + + return { + userSettings, + isShowingConfetti, + send: userSettingsService.send, + service: userSettingsService, + }; +}; diff --git a/ui-next/src/templates/JSONSchemaWorkflow.js b/ui-next/src/templates/JSONSchemaWorkflow.js new file mode 100644 index 0000000000..2ab2f7fea6 --- /dev/null +++ b/ui-next/src/templates/JSONSchemaWorkflow.js @@ -0,0 +1,314 @@ +import { JSON_SCHEMA_DRAFT_07_URL } from "utils/constants/jsonSchema"; + +export const NEW_TASK_TEMPLATE = { + name: "", + description: "", + retryCount: 3, + timeoutSeconds: 3600, + timeoutPolicy: "TIME_OUT_WF", + retryLogic: "FIXED", + retryDelaySeconds: 60, + responseTimeoutSeconds: 600, + rateLimitPerFrequency: 0, + rateLimitFrequencyInSeconds: 1, + ownerEmail: "example@email.com", + pollTimeoutSeconds: 3600, + inputKeys: [], + outputKeys: [], + inputTemplate: {}, + backoffScaleFactor: 1, + concurrentExecLimit: 0, +}; + +export const newTaskTemplate = (ownerEmail) => { + return { ...NEW_TASK_TEMPLATE, ownerEmail }; +}; + +export const NEW_WORKFLOW_TEMPLATE = { + name: "", + description: "", + version: 1, + tasks: [], + inputParameters: [], + outputParameters: {}, + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: false, + ownerEmail: "example@email.com", + timeoutPolicy: "ALERT_ONLY", + timeoutSeconds: 0, + failureWorkflow: "", +}; + +export const newWorkflowTemplate = (ownerEmail) => { + // generate random string of six characters + const suffix = Math.random().toString(36).substring(2, 7); + + return { + ...NEW_WORKFLOW_TEMPLATE, + name: `NewWorkflow_${suffix}`, + ownerEmail, + }; +}; + +export const WORKFLOW_SCHEMA = { + $schema: JSON_SCHEMA_DRAFT_07_URL, + $id: "http://example.com/example.json", + type: "object", + title: "The root schema", + description: "The root schema comprises the entire JSON document.", + default: {}, + examples: [ + { + name: "first_sample_workflow", + description: "First Sample Workflow by Orkes", + version: 1, + tasks: [ + { + name: "get_random_fact", + taskReferenceName: "get_random_fact_ref", + inputParameters: { + http_request: { + uri: "https://catfact.ninja/fact", + method: "GET", + connectionTimeOut: 3000, + readTimeOut: 3000, + }, + }, + type: "HTTP", + }, + ], + inputParameters: [], + outputParameters: { + data: "${get_random_fact_ref.output.response.body}", + }, + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: false, + ownerEmail: "example@email.com", + timeoutPolicy: "ALERT_ONLY", + timeoutSeconds: 0, + failureWorkflow: "", + }, + ], + required: ["name", "description", "version", "tasks", "schemaVersion"], + properties: { + name: { + $id: "#/properties/name", + default: "", + description: + "Workflow Name - should be without spaces or special characters. Underscores are allowed.", + examples: ["first_sample_workflow"], + maxLength: 100, + pattern: "(^\\w+$)|(^\\w[\\w|-]+\\w$)", + title: "Workflow Name", + type: "string", + }, + description: { + $id: "#/properties/description", + type: "string", + title: "Workflow Description", + description: "An brief description of your workflow for reference.", + default: "", + examples: ["First Sample Workflow"], + }, + version: { + $id: "#/properties/version", + default: 0, + description: "An explanation about the purpose of this instance.", + examples: [1], + title: "The version schema", + minimum: 1, + type: "integer", + }, + tasks: { + $id: "#/properties/tasks", + type: "array", + title: "Workflow Tasks", + description: "This list holds the tasks for your workflow.", + default: [], + examples: [ + [ + { + name: "get_random_fact", + taskReferenceName: "get_random_fact_ref", + inputParameters: { + http_request: { + uri: "https://catfact.ninja/fact", + method: "GET", + connectionTimeOut: 3000, + readTimeOut: 3000, + }, + }, + type: "HTTP", + }, + ], + ], + additionalItems: true, + items: { + $id: "#/properties/tasks/items", + anyOf: [ + { + $id: "#/properties/tasks/items/anyOf/0", + type: "object", + title: "The first anyOf schema", + description: "Workflow task details", + default: { + name: "", + taskReferenceName: "", + inputParameters: {}, + type: "SIMPLE", + }, + examples: [ + { + name: "get_random_fact", + taskReferenceName: "get_random_fact_ref", + inputParameters: { + http_request: { + uri: "https://catfact.ninja/fact", + method: "GET", + connectionTimeOut: 3000, + readTimeOut: 3000, + }, + }, + type: "HTTP", + }, + ], + required: ["name", "taskReferenceName", "inputParameters", "type"], + properties: { + name: { + $id: "#/properties/tasks/items/anyOf/0/properties/name", + type: "string", + title: "Task name", + description: "Task name", + default: "", + examples: ["get_population_data"], + }, + taskReferenceName: { + $id: "#/properties/tasks/items/anyOf/0/properties/taskReferenceName", + type: "string", + title: "Task Reference Name", + description: + "A unique task reference name for this task in the entire workflow", + default: "", + examples: ["get_population_data"], + }, + inputParameters: { + $id: "#/properties/tasks/items/anyOf/0/properties/inputParameters", + type: "object", + title: "Input Parameters", + description: "Task input parameters", + default: {}, + examples: [ + { + http_request: { + uri: "https://datausa.io/api/data?drilldowns=Nation&measures=Population", + method: "GET", + }, + }, + ], + required: [], + properties: {}, + additionalProperties: true, + }, + type: { + $id: "#/properties/tasks/items/anyOf/0/properties/type", + type: "string", + title: "Task Type", + description: "Task type", + default: "", + examples: ["HTTP"], + }, + }, + additionalProperties: true, + }, + ], + }, + }, + inputParameters: { + $id: "#/properties/inputParameters", + type: "array", + title: "Workflow Input Parameters", + description: "An explanation about the purpose of this instance.", + default: [], + examples: [[]], + additionalItems: true, + items: { + $id: "#/properties/inputParameters/items", + }, + }, + outputParameters: { + $id: "#/properties/outputParameters", + type: "object", + title: "The outputParameters schema", + description: "An explanation about the purpose of this instance.", + default: {}, + examples: [ + { + data: "${task_ref.output.dataVariable}", + source: "${task_ref.output.sourceVariable}", + }, + ], + required: [], + properties: {}, + additionalProperties: true, + }, + schemaVersion: { + $id: "#/properties/schemaVersion", + type: "integer", + title: "Schema Version", + description: "Fixed schema version", + default: 2, + examples: [2], + }, + restartable: { + $id: "#/properties/restartable", + type: "boolean", + title: "Workflow restartable", + description: "Specify if the workflow is restartable.", + default: true, + examples: [true, false], + }, + workflowStatusListenerEnabled: { + $id: "#/properties/workflowStatusListenerEnabled", + type: "boolean", + title: "The workflowStatusListenerEnabled schema", + description: "An explanation about the purpose of this instance.", + default: false, + examples: [true, false], + }, + ownerEmail: { + $id: "#/properties/ownerEmail", + type: "string", + title: "The ownerEmail schema", + description: "An explanation about the purpose of this instance.", + default: "", + examples: ["example@email.com"], + }, + timeoutPolicy: { + $id: "#/properties/timeoutPolicy", + type: "string", + title: "The timeoutPolicy schema", + description: "An explanation about the purpose of this instance.", + default: "", + examples: ["ALERT_ONLY", "TIME_OUT_WF"], + }, + timeoutSeconds: { + $id: "#/properties/timeoutSeconds", + type: "integer", + title: "The timeoutSeconds schema", + description: "An explanation about the purpose of this instance.", + default: 0, + examples: [0], + }, + failureWorkflow: { + $id: "#/properties/failureWorkflow", + type: "string", + title: "Failue Workflow Name", + description: "Specify the Failure Workflow Name.", + default: "", + examples: ["shipping_failure"], + }, + }, + additionalProperties: true, +}; diff --git a/ui-next/src/testData/diagramTests.js b/ui-next/src/testData/diagramTests.js new file mode 100644 index 0000000000..901489bfc8 --- /dev/null +++ b/ui-next/src/testData/diagramTests.js @@ -0,0 +1,3086 @@ +export const simpleDiagram = { + updateTime: 1646331692036, + name: "image_convert_resize_jim", + description: "Image Processing Workflow", + version: 1, + tasks: [ + { + name: "image_convert_resize_jim", + taskReferenceName: "image_convert_resize_ref", + inputParameters: { + fileLocation: "${workflow.input.fileLocation}", + outputFormat: "${workflow.input.recipeParameters.outputFormat}", + outputWidth: "${workflow.input.recipeParameters.outputSize.width}", + outputHeight: "${workflow.input.recipeParameters.outputSize.height}", + }, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "upload_toS3_jim", + taskReferenceName: "upload_toS3_ref", + inputParameters: { + fileLocation: "${image_convert_resize_ref.output.fileLocation}", + }, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + inputParameters: [], + outputParameters: { + fileLocation: "${upload_toS3_ref.output.fileLocation}", + }, + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: true, + ownerEmail: "devrel@orkes.io", + timeoutPolicy: "ALERT_ONLY", + timeoutSeconds: 0, + failureWorkflow: "", + variables: {}, + inputTemplate: {}, +}; + +export const populationMinMax = { + updateTime: 1645990260050, + name: "PopulationMinMax", + description: "Min Max Population", + version: 1, + tasks: [ + { + name: "get_population_data", + taskReferenceName: "get_population_data_ref", + inputParameters: { + http_request: { + uri: "https://datausa.io/api/data?drilldowns=State&measures=Population&year=latest", + method: "GET", + }, + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "fork_join", + taskReferenceName: "fork_ref", + inputParameters: {}, + type: "FORK_JOIN", + decisionCases: {}, + defaultCase: [], + forkTasks: [ + [ + { + name: "process_population_max", + taskReferenceName: "process_population_max_ref", + inputParameters: { + body: "${get_population_data_ref.output.response.body}", + queryExpression: "[.body.data[]] | max_by(.Population)", + }, + type: "JSON_JQ_TRANSFORM", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + [ + { + name: "process_population_min", + taskReferenceName: "process_population_min_ref", + inputParameters: { + body: "${get_population_data_ref.output.response.body}", + queryExpression: "[.body.data[]] | min_by(.Population)", + }, + type: "JSON_JQ_TRANSFORM", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + ], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "join", + taskReferenceName: "join_ref", + inputParameters: {}, + type: "JOIN", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: ["process_population_max_ref", "process_population_min_ref"], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + inputParameters: [], + outputParameters: { + maxPopulation: "${process_population_max_ref.output.result}", + minPopulation: "${process_population_min_ref.output.result}", + }, + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: false, + ownerEmail: "developers@orkes.io", + timeoutPolicy: "ALERT_ONLY", + timeoutSeconds: 0, + failureWorkflow: "", + variables: {}, + inputTemplate: {}, +}; + +export const decisionSample = { + updateTime: 1636597950018, + name: "exclusive_join", + description: "Exclusive Join Example", + version: 1, + tasks: [ + { + type: "TERMINAL", + name: "start", + taskReferenceName: "__start", + }, + { + name: "api_decision", + taskReferenceName: "api_decision_ref", + inputParameters: { + case_value_param: "${workflow.input.type}", + }, + type: "DECISION", + caseValueParam: "case_value_param", + decisionCases: { + POST: [ + { + name: "get_posts", + taskReferenceName: "get_posts_ref", + inputParameters: { + http_request: { + uri: "https://jsonplaceholder.typicode.com/posts/1", + method: "GET", + }, + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + COMMENT: [ + { + name: "get_post_comments", + taskReferenceName: "get_post_comments_ref", + inputParameters: { + http_request: { + uri: "https://jsonplaceholder.typicode.com/comments?postId=1", + method: "GET", + }, + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + USER: [ + { + name: "get_user_posts", + taskReferenceName: "get_user_posts_ref", + inputParameters: { + http_request: { + uri: "https://jsonplaceholder.typicode.com/posts?userId=1", + method: "GET", + }, + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + }, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "notification_join", + taskReferenceName: "notification_join_ref", + inputParameters: {}, + type: "EXCLUSIVE_JOIN", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: ["get_posts_ref", "get_post_comments_ref"], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + type: "TERMINAL", + name: "final", + taskReferenceName: "__final", + }, + ], + inputParameters: [], + outputParameters: {}, + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: true, + ownerEmail: "encode_admin@test.com", + timeoutPolicy: "ALERT_ONLY", + timeoutSeconds: 0, + failureWorkflow: "", + variables: {}, + inputTemplate: {}, +}; + +export const complexDiagram = { + createTime: 1639691367677, + updateTime: 1641859692443, + name: "port_in_wf", + description: "Port In Workflow", + version: 1, + tasks: [ + { + type: "TERMINAL", + name: "start", + taskReferenceName: "__start", + }, + { + name: "Submit To ITG with Retry", + taskReferenceName: "submit_to_itg_with_retry", + inputParameters: { + value: "${workflow.input.iterations}", + terminate: "${workflow.variables.terminate_loop}", + }, + type: "DO_WHILE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopCondition: + "if ( ($.submit_to_itg_with_retry['iteration'] < $.value) && !$.terminate) { true; } else { false; }", + loopOver: [ + { + name: "Submit to ITG", + taskReferenceName: "submit_to_itg", + inputParameters: { + http_request: { + uri: "https://jsonplaceholder.typicode.com/todos/${$.workflow.input.iterations}", + method: "GET", + }, + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: true, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "Check Status", + taskReferenceName: "check_status", + inputParameters: { + prev_task_result: "${submit_to_itg.output}", + switchCaseValue: "${submit_to_itg.status}", + }, + type: "DECISION", + caseValueParam: "switchCaseValue", + decisionCases: { + COMPLETED: [ + { + name: "Complete Request Loop", + taskReferenceName: "complete_loop_success", + inputParameters: { + terminate_loop: true, + success: true, + }, + type: "SET_VARIABLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + COMPLETED_WITH_ERRORS: [ + { + name: "Retry HTTP Request", + taskReferenceName: "retry_http_request", + inputParameters: { + terminate_loop: false, + success: false, + }, + type: "SET_VARIABLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "Update Records", + taskReferenceName: "update_records_on_retry", + inputParameters: { + update_records_on_retry: 1, + }, + type: "SET_VARIABLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + }, + defaultCase: [ + { + name: "Permanent Failure", + taskReferenceName: "terminate_loop", + inputParameters: { + terminate_loop: true, + success: false, + }, + type: "SET_VARIABLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "Update Records Terminate", + taskReferenceName: "update_records_on_failure", + inputParameters: { + update_records_on_retry: 1, + }, + type: "SET_VARIABLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "Terminate Workflow", + taskReferenceName: "terminate_on_perm_failure", + inputParameters: { + terminationStatus: "FAILED", + workflowOutput: + "Failing workflow as retries exhausted and failures are marked as permenant", + }, + type: "TERMINATE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + }, + { + name: "Check If Success", + taskReferenceName: "check_success", + inputParameters: { + switchCaseValue: "${workflow.variables.success}", + }, + type: "DECISION", + caseValueParam: "switchCaseValue", + decisionCases: { + false: [ + { + name: "Update Records on Failure", + taskReferenceName: "update_records_on_failure", + inputParameters: { + update_records_on_retry: 2, + }, + type: "SET_VARIABLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "Terminate Workflow", + taskReferenceName: "terminate_on_perm_failure2", + inputParameters: { + terminationStatus: "FAILED", + workflowOutput: + "Failing workflow as retries exhausted and failures are marked as permenant", + }, + type: "TERMINATE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + }, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "Wait for the async message response", + taskReferenceName: "wait_for_response", + inputParameters: {}, + type: "WAIT", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "Check Response", + taskReferenceName: "check_response_succeeded", + inputParameters: { + switchCaseValue: "${wait_for_response.output.success}", + }, + type: "DECISION", + caseValueParam: "switchCaseValue", + decisionCases: { + false: [ + { + name: "Update Records on ITGH Failure", + taskReferenceName: "update_records_on_itg_failure", + inputParameters: { + response: "${wait_for_response.output}", + }, + type: "SET_VARIABLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "Terminate Workflow", + taskReferenceName: "terminate_on_response_failure", + inputParameters: { + terminationStatus: "FAILED", + workflowOutput: + "Failing workflow as retries exhausted and failures are marked as permenant", + }, + type: "TERMINATE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + }, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + type: "TERMINAL", + name: "final", + taskReferenceName: "__final", + }, + ], + inputParameters: [], + outputParameters: {}, + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: false, + ownerEmail: "example@email.com", + timeoutPolicy: "ALERT_ONLY", + timeoutSeconds: 0, + failureWorkflow: "", + variables: { + success: false, + }, + inputTemplate: {}, +}; + +export const simpleLoopSample = { + updateTime: 1638843682276, + name: "test_looping_concurrency", + description: "Test Looping", + version: 1, + tasks: [ + { + type: "TERMINAL", + name: "start", + taskReferenceName: "__start", + }, + { + name: "fork_join", + taskReferenceName: "my_fork_join_ref", + inputParameters: {}, + type: "FORK_JOIN", + decisionCases: {}, + defaultCase: [], + forkTasks: [ + [ + { + name: "loop_1", + taskReferenceName: "loop_1", + inputParameters: {}, + type: "DO_WHILE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopCondition: "(($.loop_1['iteration'] < 10))", + loopOver: [ + { + name: "first_task_in_loop", + taskReferenceName: "loop_1_task_iter", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "loop_1_set_var", + taskReferenceName: "loop_1_sv", + inputParameters: { + name: "Orkes", + }, + type: "SET_VARIABLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + }, + ], + [ + { + name: "loop_2", + taskReferenceName: "loop_2", + inputParameters: {}, + type: "DO_WHILE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopCondition: "(($.loop_2['iteration'] < 10))", + loopOver: [ + { + name: "first_task_in_loop", + taskReferenceName: "loop_2_task_iter", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "loop_2_set_var", + taskReferenceName: "loop_2_sv", + inputParameters: { + name: "Orkes", + }, + type: "SET_VARIABLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + }, + ], + [ + { + name: "loop_3", + taskReferenceName: "loop_3", + inputParameters: { + value: "${workflow.input.value}", + }, + type: "DO_WHILE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopCondition: + "(($.loop_3['iteration'] < $.value ) && ( $.loop_3_task_iter['outputVal'] < 10))", + loopOver: [ + { + name: "first_task_in_loop", + taskReferenceName: "loop_3_task_iter", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + }, + ], + ], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "fork_join", + taskReferenceName: "fork_join_ref", + inputParameters: {}, + type: "JOIN", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: ["loop_1", "loop_2", "loop_3"], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + type: "TERMINAL", + name: "final", + taskReferenceName: "__final", + }, + ], + inputParameters: [], + outputParameters: {}, + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: false, + ownerEmail: "builds@orkes.io", + timeoutPolicy: "ALERT_ONLY", + timeoutSeconds: 0, + failureWorkflow: "", + variables: {}, + inputTemplate: {}, +}; + +export const kitchenSink = { + createTime: 1647439893125, + name: "kitchensink", + description: "kitchensink workflow", + version: 1, + tasks: [ + { + name: "task_1", + taskReferenceName: "task_1", + inputParameters: { + mod: "${workflow.input.mod}", + oddEven: "${workflow.input.oddEven}", + }, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "event_task", + taskReferenceName: "event_0", + inputParameters: { + mod: "${workflow.input.mod}", + oddEven: "${workflow.input.oddEven}", + }, + type: "EVENT", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + sink: "conductor", + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "dyntask", + taskReferenceName: "task_2", + inputParameters: { + taskToExecute: "${workflow.input.task2Name}", + }, + type: "DYNAMIC", + dynamicTaskNameParam: "taskToExecute", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "oddEvenDecision", + taskReferenceName: "oddEvenDecision", + inputParameters: { + oddEven: "${task_2.output.oddEven}", + }, + type: "DECISION", + caseValueParam: "oddEven", + decisionCases: { + 0: [ + { + name: "task_4", + taskReferenceName: "task_4", + inputParameters: { + mod: "${task_2.output.mod}", + oddEven: "${task_2.output.oddEven}", + }, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "dynamic_fanout", + taskReferenceName: "fanout1", + inputParameters: { + dynamicTasks: "${task_4.output.dynamicTasks}", + input: "${task_4.output.inputs}", + }, + type: "FORK_JOIN_DYNAMIC", + decisionCases: {}, + dynamicForkTasksParam: "dynamicTasks", + dynamicForkTasksInputParamName: "input", + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "dynamic_join", + taskReferenceName: "join1", + inputParameters: {}, + type: "JOIN", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + 1: [ + { + name: "fork_join", + taskReferenceName: "forkx", + inputParameters: {}, + type: "FORK_JOIN", + decisionCases: {}, + defaultCase: [], + forkTasks: [ + [ + { + name: "task_10", + taskReferenceName: "task_10", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "sub_workflow_x", + taskReferenceName: "wf3", + inputParameters: { + mod: "${task_1.output.mod}", + oddEven: "${task_1.output.oddEven}", + }, + type: "SUB_WORKFLOW", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + subWorkflowParam: { + name: "sub_flow_1", + version: 1, + }, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + [ + { + name: "task_11", + taskReferenceName: "task_11", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "sub_workflow_x", + taskReferenceName: "wf4", + inputParameters: { + mod: "${task_1.output.mod}", + oddEven: "${task_1.output.oddEven}", + }, + type: "SUB_WORKFLOW", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + subWorkflowParam: { + name: "sub_flow_1", + version: 1, + }, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + ], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "join", + taskReferenceName: "join2", + inputParameters: {}, + type: "JOIN", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: ["wf3", "wf4"], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + }, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "search_elasticsearch", + taskReferenceName: "get_es_1", + inputParameters: { + http_request: { + uri: "http://localhost:9200/conductor/_search?size=10", + method: "GET", + }, + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "task_30", + taskReferenceName: "task_30", + inputParameters: { + statuses: "${get_es_1.output..status}", + workflowIds: "${get_es_1.output..workflowId}", + }, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + inputParameters: [], + outputParameters: {}, + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: false, + ownerEmail: "builds@orkes.io", + timeoutPolicy: "ALERT_ONLY", + timeoutSeconds: 0, + failureWorkflow: "", + variables: {}, + inputTemplate: {}, +}; + +export const switchExample = { + updateTime: 1635487924982, + name: "Switch_TaskExample", + description: "Switch_TaskExample", + version: 1, + tasks: [ + { + type: "TERMINAL", + name: "start", + taskReferenceName: "__start", + }, + { + name: "switch_task", + taskReferenceName: "switch_task", + inputParameters: { + case_value_param: "${workflow.input.number}", + }, + type: "SWITCH", + decisionCases: { + 0: [ + { + name: "task_5", + taskReferenceName: "task_5", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "task_6", + taskReferenceName: "task_6", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + 1: [ + { + name: "task_8", + taskReferenceName: "task_8", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "task_10", + taskReferenceName: "task_10", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + }, + defaultCase: [ + { + name: "task_8", + taskReferenceName: "task_8_default", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "case_value_param", + }, + { + name: "task_10", + taskReferenceName: "task_10_last", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + type: "TERMINAL", + name: "final", + taskReferenceName: "__final", + }, + ], + inputParameters: [], + outputParameters: {}, + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: true, + ownerEmail: "abc@example.com", + timeoutPolicy: "ALERT_ONLY", + timeoutSeconds: 0, + failureWorkflow: "", + variables: {}, + inputTemplate: {}, +}; + +export const switchTasksWithTerminationNodes = { + name: "loan_type", + taskReferenceName: "loan_type", + inputParameters: { + loantype: "${customer_details.output.loantype}", + }, + type: "SWITCH", + decisionCases: { + education: [ + { + name: "education_details", + taskReferenceName: "education_details", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "terminate", + taskReferenceName: "terminate0", + inputParameters: { + terminationStatus: "COMPLETED", + workflowOutput: { result: "${task0.output}" }, + }, + type: "TERMINATE", + startDelay: 0, + optional: false, + }, + ], + property: [ + { + name: "employment_details", + taskReferenceName: "employment_details", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "terminate", + taskReferenceName: "terminate1", + inputParameters: { + terminationStatus: "COMPLETED", + workflowOutput: { result: "${task0.output}" }, + }, + type: "TERMINATE", + startDelay: 0, + optional: false, + }, + ], + }, + defaultCase: [ + { + name: "business_details", + taskReferenceName: "business_details", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "terminate", + taskReferenceName: "terminate2", + inputParameters: { + terminationStatus: "COMPLETED", + workflowOutput: { result: "${task0.output}" }, + }, + type: "TERMINATE", + startDelay: 0, + optional: false, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "loantype", +}; + +export const lonleySwitchTask = { + name: "loan_type", + taskReferenceName: "loan_type", + inputParameters: { + loantype: "${customer_details.output.loantype}", + }, + type: "SWITCH", + decisionCases: { + emptyCase: [], + education: [ + { + name: "education_details", + taskReferenceName: "education_details", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "education_details_verification", + taskReferenceName: "education_details_verification", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + property: [ + { + name: "employment_details", + taskReferenceName: "employment_details", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "employment_details_verification", + taskReferenceName: "employment_details_verification", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + business: [ + { + name: "business_details", + taskReferenceName: "business_details", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "business_details_verification", + taskReferenceName: "business_details_verification", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + }, + defaultCase: [ + { + name: "business_details", + taskReferenceName: "business_details", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "business_details_verification", + taskReferenceName: "business_details_verification", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "loantype", +}; + +export const forkJoinTask = { + name: "fork_join", + taskReferenceName: "fork_ref", + inputParameters: {}, + type: "FORK_JOIN", + decisionCases: {}, + defaultCase: [], + forkTasks: [ + [ + { + name: "process_population_max", + taskReferenceName: "process_population_max_ref", + inputParameters: { + body: "${get_population_data_ref.output.response.body}", + queryExpression: "[.body.data[]] | max_by(.Population)", + }, + type: "JSON_JQ_TRANSFORM", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + [ + { + name: "process_population_min", + taskReferenceName: "process_population_min_ref", + inputParameters: { + body: "${get_population_data_ref.output.response.body}", + queryExpression: "[.body.data[]] | min_by(.Population)", + }, + type: "JSON_JQ_TRANSFORM", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + ], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], +}; + +export const loanBanking = { + createTime: 1658508370400, + updateTime: 1649266893306, + name: "loan_banking", + description: "This workflow is to demo the loan banking process", + version: 7, + tasks: [ + { + name: "customer_details", + taskReferenceName: "customer_details", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "loan_type", + taskReferenceName: "loan_type", + inputParameters: { + loantype: "${customer_details.output.loantype}", + }, + type: "SWITCH", + decisionCases: { + education: [ + { + name: "education_details", + taskReferenceName: "education_details", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "education_details_verification", + taskReferenceName: "education_details_verification", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + property: [ + { + name: "employment_details", + taskReferenceName: "employment_details", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "employment_details_verification", + taskReferenceName: "employment_details_verification", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + }, + defaultCase: [ + { + name: "business_details", + taskReferenceName: "business_details", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "business_details_verification", + taskReferenceName: "business_details_verification", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "value-param", + expression: "loantype", + }, + { + name: "credit_score_risk", + taskReferenceName: "credit_score_risk", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "credit_result", + taskReferenceName: "credit_result", + inputParameters: { + creditScore: "${credit_score_risk.output.creditScore}", + }, + type: "SWITCH", + decisionCases: { + possible: [ + { + name: "principal_interest_calculation", + taskReferenceName: "principal_interest_calculation", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "customer_decision", + taskReferenceName: "customer_decision", + inputParameters: { + decision: "${loan_offered_to_customer.output.decision}", + }, + type: "SWITCH", + decisionCases: { + yes: [ + { + name: "loan_transfer_to_customer_account", + taskReferenceName: "loan_transfer_to_customer_account", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + }, + defaultCase: [ + { + name: "terminate_due_to_customer_rejection", + taskReferenceName: "terminate_due_to_customer_rejection", + inputParameters: { + terminationStatus: "COMPLETED", + }, + type: "TERMINATE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "javascript", + expression: "$.decision=='yes' ? 'yes' : 'no' ", + }, + ], + }, + defaultCase: [ + { + name: "terminate_due_to_bank_rejection", + taskReferenceName: "terminate_due_to_bank_rejection", + inputParameters: { + terminationStatus: "COMPLETED", + }, + type: "TERMINATE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "javascript", + expression: "$.creditScore > 760 ? 'possible' : 'reject' ", + }, + ], + inputParameters: [], + outputParameters: {}, + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: false, + ownerEmail: "a8615897-dfaf-4d7d-a9ed-f3f78f7ef094@apps.orkes.io", + timeoutPolicy: "ALERT_ONLY", + timeoutSeconds: 0, + failureWorkflow: "", + variables: {}, + inputTemplate: {}, +}; + +export const switchTaskCorrectlyTerminated = { + name: "credit_result", + taskReferenceName: "credit_result", + inputParameters: { + creditScore: "${credit_score_risk.output.creditScore}", + }, + type: "SWITCH", + decisionCases: { + possible: [ + { + name: "principal_interest_calculation", + taskReferenceName: "principal_interest_calculation", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "loan_offered_to_customer", + taskReferenceName: "loan_offered_to_customer", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "terminate_due_to_customer_rejection", + taskReferenceName: "terminate_due_to_customer_rejection_b", + inputParameters: { + terminationStatus: "COMPLETED", + }, + type: "TERMINATE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + }, + defaultCase: [ + { + name: "terminate_due_to_customer_rejection", + taskReferenceName: "terminate_due_to_customer_rejection", + inputParameters: { + terminationStatus: "COMPLETED", + }, + type: "TERMINATE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "javascript", + expression: "$.decision=='yes' ? 'yes' : 'no' ", +}; + +export const switchTaskOneNotTerminated = { + name: "credit_result", + taskReferenceName: "credit_result", + inputParameters: { + creditScore: "${credit_score_risk.output.creditScore}", + }, + type: "SWITCH", + decisionCases: { + possible: [ + { + name: "principal_interest_calculation", + taskReferenceName: "principal_interest_calculation", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "loan_offered_to_customer", + taskReferenceName: "loan_offered_to_customer", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + }, + defaultCase: [ + { + name: "terminate_due_to_customer_rejection", + taskReferenceName: "terminate_due_to_customer_rejection", + inputParameters: { + terminationStatus: "COMPLETED", + }, + type: "TERMINATE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "javascript", + expression: "$.decision=='yes' ? 'yes' : 'no' ", +}; + +export const switchWithinSwitchLeafNotTerminated = { + name: "credit_result", + taskReferenceName: "credit_result", + inputParameters: { + creditScore: "${credit_score_risk.output.creditScore}", + }, + type: "SWITCH", + decisionCases: { + possible: [ + { + name: "principal_interest_calculation", + taskReferenceName: "principal_interest_calculation", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "loan_offered_to_customer", + taskReferenceName: "loan_offered_to_customer", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "customer_decision", + taskReferenceName: "customer_decision", + inputParameters: { + decision: "${loan_offered_to_customer.output.decision}", + }, + type: "SWITCH", + decisionCases: { + yes: [ + { + name: "loan_transfer_to_customer_account", + taskReferenceName: "loan_transfer_to_customer_account", + inputParameters: {}, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + }, + defaultCase: [ + { + name: "terminate_due_to_customer_rejection", + taskReferenceName: "terminate_due_to_customer_rejection", + inputParameters: { + terminationStatus: "COMPLETED", + }, + type: "TERMINATE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "javascript", + expression: "$.decision=='yes' ? 'yes' : 'no' ", + }, + ], + }, + defaultCase: [ + { + name: "terminate_due_to_bank_rejection", + taskReferenceName: "terminate_due_to_bank_rejection", + inputParameters: { + terminationStatus: "COMPLETED", + }, + type: "TERMINATE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "javascript", + expression: "$.creditScore > 760 ? 'possible' : 'reject' ", +}; + +export const taskStub = { + id: "Get_repo_details_ref", + text: "Get_repo_details", + data: { + task: { + name: "Get_repo_details", + taskReferenceName: "Get_repo_details_ref", + inputParameters: { + http_request: { + uri: "https://api.github.com/repos/${workflow.input.gh_account}/${workflow.input.gh_repo}", + method: "GET", + headers: { + Authorization: "token ${workflow.input.gh_token}", + Accept: "application/vnd.github.v3.star+json", + }, + connectionTimeOut: 2000, + readTimeOut: 2000, + }, + asyncComplete: false, + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + executionData: { + status: "COMPLETED", + executed: true, + attempts: 1, + }, + }, + crumbs: [ + { + parent: null, + ref: "calculate_start_cutoff_ref", + refIdx: 0, + }, + { + parent: null, + ref: "Get_repo_details_ref", + refIdx: 1, + }, + ], + status: "COMPLETED", + executed: true, + attempts: 1, + selected: false, + }, + width: 350, + height: 130, +}; + +export const unConnectedSwitchTask = { + name: "sample_task_name_switch", + taskReferenceName: "sample_task_name_h64r7_ref", + inputParameters: { + switchCaseValue: "", + }, + type: "SWITCH", + decisionCases: {}, + defaultCase: [], + evaluatorType: "value-param", + expression: "switchCaseValue", +}; + +export const unConnectedSwitch = { + name: "unconnectedWF", + description: + "Edit or extend this sample workflow. Set the workflow name to get started", + version: 1, + tasks: [ + { + name: "sample_task_name_event", + taskReferenceName: "sample_task_name_4a2rf_ref", + type: "EVENT", + sink: "conductor:internal_event_name", + }, + unConnectedSwitchTask, + { + name: "get_random_fact", + taskReferenceName: "get_random_fact", + inputParameters: { + http_request: { + uri: "https://catfact.ninja/fact", + method: "GET", + connectionTimeOut: 3000, + readTimeOut: 3000, + }, + }, + type: "HTTP", + }, + ], + inputParameters: [], + outputParameters: { + data: "${get_random_fact.output.response.body.fact}", + }, + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: false, + ownerEmail: "james.stuart@orkes.io", + timeoutPolicy: "ALERT_ONLY", + timeoutSeconds: 0, +}; + +export const switchTaskWithADecisionButNoTerminateTasks = { + name: "sample_task_name_switch", + taskReferenceName: "sample_task_name_h64r7_ref", + inputParameters: { + switchCaseValue: "", + }, + type: "SWITCH", + decisionCases: { + some_case: [ + { + name: "sample_task_name_http", + taskReferenceName: "sample_task_name_yioskj_ref", + type: "HTTP", + inputParameters: { + http_request: { + uri: "https://orkes-api-tester.orkesconductor.com/get", + method: "GET", + }, + }, + }, + ], + }, + defaultCase: [], + evaluatorType: "value-param", + expression: "switchCaseValue", +}; + +export const workflowWithASwitchWithoutTermination = { + name: "unconnectedWF", + description: + "Edit or extend this sample workflow. Set the workflow name to get started", + version: 1, + tasks: [ + { + name: "sample_task_name_event", + taskReferenceName: "sample_task_name_4a2rf_ref", + type: "EVENT", + sink: "conductor:internal_event_name", + }, + switchTaskWithADecisionButNoTerminateTasks, + { + name: "last_task", + taskReferenceName: "last_task", + inputParameters: { + http_request: { + uri: "https://catfact.ninja/fact", + method: "GET", + connectionTimeOut: 3000, + readTimeOut: 3000, + }, + }, + type: "HTTP", + }, + ], + inputParameters: [], + outputParameters: { + data: "${get_random_fact.output.response.body.fact}", + }, + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: false, + ownerEmail: "james.stuart@orkes.io", + timeoutPolicy: "ALERT_ONLY", + timeoutSeconds: 0, +}; + +export const workflowWithSwitchWithinSwitchUnterminated = { + name: "nestedSwitch", + description: + "Edit or extend this sample workflow. Set the workflow name to get started", + version: 1, + tasks: [ + { + name: "sample_task_name_event", + taskReferenceName: "sample_task_name_4a2rf_ref", + type: "EVENT", + sink: "conductor:internal_event_name", + }, + { + name: "sample_task_name_switch", + taskReferenceName: "sample_task_name_h64r7_ref", + inputParameters: { + switchCaseValue: "", + }, + type: "SWITCH", + decisionCases: { + new_case_a65un: [ + { + name: "sample_task_name_http", + taskReferenceName: "sample_task_name_yioskj_ref", + type: "HTTP", + inputParameters: { + http_request: { + uri: "https://orkes-api-tester.orkesconductor.com/get", + method: "GET", + }, + }, + }, + ], + case_going_to_switch: [ + { + name: "sample_task_name_switch", + taskReferenceName: "sample_task_name_lpskl_ref", + inputParameters: { + switchCaseValue: "", + }, + type: "SWITCH", + decisionCases: { + nestedCase: [ + { + name: "sample_task_name_dynamic", + taskReferenceName: "sample_task_name_8sio6i_ref", + inputParameters: { + taskToExecute: "", + }, + type: "DYNAMIC", + dynamicTaskNameParam: "taskToExecute", + }, + ], + }, + defaultCase: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + }, + ], + }, + defaultCase: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + }, + { + name: "last_task", + taskReferenceName: "last_task", + inputParameters: { + http_request: { + uri: "https://catfact.ninja/fact", + method: "GET", + connectionTimeOut: 3000, + readTimeOut: 3000, + }, + }, + type: "HTTP", + }, + ], + inputParameters: [], + outputParameters: { + data: "${get_random_fact.output.response.body.fact}", + }, + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: false, + ownerEmail: "james.stuart@orkes.io", + timeoutPolicy: "ALERT_ONLY", + timeoutSeconds: 0, +}; + +export const wfWithWhileWithSubWorkflow = { + createTime: 1662500473363, + updateTime: 1662503566660, + name: "wf_with_while_with_sub", + description: "Im changing the name now", + version: 1, + tasks: [ + { + name: "get_random_fact", + taskReferenceName: "get_random_fact", + inputParameters: { + http_request: { + uri: "https://catfact.ninja/fact", + method: "GET", + connectionTimeOut: 3000, + readTimeOut: 3000, + }, + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "sample_task_name_do_while", + taskReferenceName: "sample_task_name_do_while_yy67c_ref", + inputParameters: {}, + type: "DO_WHILE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopCondition: "", + loopOver: [ + { + name: "sample_task_name_sub_workflow", + taskReferenceName: "sample_task_name_sub_workflow_ref", + inputParameters: {}, + type: "SUB_WORKFLOW", + subWorkflowParam: { + name: "testing_new_two", + version: 1, + }, + }, + ], + }, + ], + inputParameters: [], + outputParameters: { + data: "${get_random_fact.output.response.body.fact}", + }, + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: false, + ownerEmail: "james.stuart@orkes.io", + timeoutPolicy: "ALERT_ONLY", + timeoutSeconds: 0, + variables: {}, + inputTemplate: {}, +}; + +export const subWorkflowWithinAFork = { + createTime: 1662500473363, + updateTime: 1662503566660, + name: "sub_workflow_within_a_fork", + description: "Im changing the name now", + version: 1, + tasks: [ + { + name: "get_random_fact", + taskReferenceName: "get_random_fact", + inputParameters: { + http_request: { + uri: "https://catfact.ninja/fact", + method: "GET", + connectionTimeOut: 3000, + readTimeOut: 3000, + }, + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "sample_task_name_fork", + taskReferenceName: "sample_task_name_fork_8ksay_ref", + inputParameters: {}, + type: "FORK_JOIN", + decisionCases: {}, + defaultCase: [], + forkTasks: [ + [ + { + name: "sample_task_name_event", + taskReferenceName: "sample_task_name_event_rws94_ref", + type: "EVENT", + sink: "conductor:internal_event_name", + }, + ], + [ + { + name: "sample_task_name_sub_workflow", + taskReferenceName: "sample_task_name_sub_workflow_ref", + inputParameters: {}, + type: "SUB_WORKFLOW", + subWorkflowParam: { + name: "testing_new_two", + version: 1, + }, + }, + ], + ], + }, + { + name: "sample_task_name_join", + taskReferenceName: "sample_task_name_join_8rx5b_ref", + inputParameters: {}, + type: "JOIN", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + asyncComplete: false, + }, + ], + inputParameters: [], + outputParameters: { + data: "${get_random_fact.output.response.body.fact}", + }, + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: false, + ownerEmail: "james.stuart@orkes.io", + timeoutPolicy: "ALERT_ONLY", + timeoutSeconds: 0, + variables: {}, + inputTemplate: {}, +}; + +export const workflowWithUnknownType = { + updateTime: 1646331692036, + name: "someWfName", + description: "Image Processing Workflow", + version: 1, + tasks: [ + { + name: "image_convert_resize_jim", + taskReferenceName: "image_convert_resize_ref", + inputParameters: { + fileLocation: "${workflow.input.fileLocation}", + outputFormat: "${workflow.input.recipeParameters.outputFormat}", + outputWidth: "${workflow.input.recipeParameters.outputSize.width}", + outputHeight: "${workflow.input.recipeParameters.outputSize.height}", + }, + type: "UNKNOWN_TYPE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + { + name: "upload_toS3_jim", + taskReferenceName: "upload_toS3_ref", + inputParameters: { + fileLocation: "${image_convert_resize_ref.output.fileLocation}", + }, + type: "SIMPLE", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + }, + ], + inputParameters: [], + outputParameters: { + fileLocation: "${upload_toS3_ref.output.fileLocation}", + }, + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: true, + ownerEmail: "devrel@orkes.io", + timeoutPolicy: "ALERT_ONLY", + timeoutSeconds: 0, + variables: {}, + inputTemplate: {}, +}; + +export const nestedForkJoin = { + name: "NewWorkflow_qcwhb", + description: + "Edit or extend this sample workflow. Set the workflow name to get started", + version: 1, + tasks: [ + { + name: "get_random_fact", + taskReferenceName: "get_random_fact", + inputParameters: { + http_request: { + uri: "https://catfact.ninja/fact", + method: "GET", + connectionTimeOut: 3000, + readTimeOut: 3000, + }, + }, + type: "HTTP", + }, + { + name: "sample_task_name_fork_ytrlak_ref", + taskReferenceName: "sample_task_name_fork_ytrlak_ref", + inputParameters: {}, + type: "FORK_JOIN", + decisionCases: {}, + defaultCase: [], + forkTasks: [ + [ + { + name: "sample_task_name_http_u9mzs_ref", + taskReferenceName: "sample_task_name_http_u9mzs_ref", + type: "HTTP", + inputParameters: { + http_request: { + uri: "https://orkes-api-tester.orkesconductor.com/get", + method: "GET", + connectionTimeOut: 3000, + readTimeOut: 3000, + }, + }, + }, + ], + [ + { + name: "sample_task_name_event_erts_ref", + taskReferenceName: "sample_task_name_event_erts_ref", + type: "EVENT", + sink: "conductor:internal_event_name", + }, + ], + ], + }, + { + name: "sample_task_name_join_fd9v1_ref", + taskReferenceName: "sample_task_name_join_fd9v1_ref", + inputParameters: {}, + type: "JOIN", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: ["sample_task_name_http_u9mzs_ref"], + optional: false, + asyncComplete: false, + }, + { + name: "sample_task_name_http_mvwvv_ref", + taskReferenceName: "sample_task_name_http_mvwvv_ref", + type: "HTTP", + inputParameters: { + http_request: { + uri: "https://orkes-api-tester.orkesconductor.com/get", + method: "GET", + connectionTimeOut: 3000, + readTimeOut: 3000, + }, + }, + }, + { + name: "sample_task_name_join_a75or_ref", + taskReferenceName: "sample_task_name_join_a75or_ref", + inputParameters: {}, + type: "JOIN", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: ["sample_task_name_event_erts_ref"], + optional: false, + asyncComplete: false, + }, + { + name: "sample_task_name_fork_6vg5rj_ref", + taskReferenceName: "sample_task_name_fork_6vg5rj_ref", + inputParameters: {}, + type: "FORK_JOIN", + decisionCases: {}, + defaultCase: [], + forkTasks: [ + [ + { + name: "sample_task_name_kafka_publish_h9bubk_ref", + taskReferenceName: "sample_task_name_kafka_publish_h9bubk_ref", + type: "KAFKA_PUBLISH", + inputParameters: { + kafka_request: { + topic: "userTopic", + value: "Message to publish", + bootStrapServers: "localhost:9092", + headers: { + "X-Auth": "Auth-key", + }, + key: "123", + keySerializer: + "org.apache.kafka.common.serialization.IntegerSerializer", + }, + }, + }, + ], + [ + { + name: "sample_task_name_http_wh3oz_ref", + taskReferenceName: "sample_task_name_http_wh3oz_ref", + type: "HTTP", + inputParameters: { + http_request: { + uri: "https://orkes-api-tester.orkesconductor.com/get", + method: "GET", + connectionTimeOut: 3000, + readTimeOut: 3000, + }, + }, + }, + ], + ], + }, + { + name: "sample_task_name_join_6fc3tf_ref", + taskReferenceName: "sample_task_name_join_6fc3tf_ref", + inputParameters: {}, + type: "JOIN", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + asyncComplete: false, + }, + { + name: "sample_task_name_switch_pm7wsj_ref", + taskReferenceName: "sample_task_name_switch_pm7wsj_ref", + inputParameters: { + switchCaseValue: "", + }, + type: "SWITCH", + decisionCases: { + new_case_ms0jy: [ + { + name: "sample_task_name_simple_0xdkv_ref", + taskReferenceName: "sample_task_name_simple_0xdkv_ref", + type: "SIMPLE", + }, + { + name: "sample_task_name_fork_lx82h_ref", + taskReferenceName: "sample_task_name_fork_lx82h_ref", + inputParameters: {}, + type: "FORK_JOIN", + decisionCases: {}, + defaultCase: [], + forkTasks: [ + [ + { + name: "sample_task_name_inline_knvwp_ref", + taskReferenceName: "sample_task_name_inline_knvwp_ref", + type: "INLINE", + inputParameters: { + expression: "(function(){ return $.value1 + $.value2;})();", + evaluatorType: "graaljs", + value1: 1, + value2: 2, + }, + }, + ], + ], + }, + { + name: "sample_task_name_join_uqholl_ref", + taskReferenceName: "sample_task_name_join_uqholl_ref", + inputParameters: {}, + type: "JOIN", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + asyncComplete: false, + }, + ], + }, + defaultCase: [], + evaluatorType: "value-param", + expression: "switchCaseValue", + }, + ], + inputParameters: [], + outputParameters: { + data: "${get_random_fact.output.response.body.fact}", + }, + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: false, + ownerEmail: "james.stuart@orkes.io", + timeoutPolicy: "ALERT_ONLY", + timeoutSeconds: 0, +}; + +export const unknownTaskTypeWf = { + name: "NewWorkflow_6ns8k", + description: "", + version: 1, + tasks: [ + { + name: "event_task", + taskReferenceName: "event_task_ref", + type: "SOME_RANDOM_WRONG_TYPE", + sink: "sqs:internal_event_name", + inputParameters: {}, + }, + ], + inputParameters: [], + outputParameters: {}, + schemaVersion: 2, + restartable: true, + workflowStatusListenerEnabled: false, + ownerEmail: "najeeb.thangal@orkes.io", + timeoutPolicy: "ALERT_ONLY", + timeoutSeconds: 0, + failureWorkflow: "", +}; + +export const switchExecutionDefaultByEvaluationResultNull = { + name: "pin_validation", + taskReferenceName: "pin_validation", + inputParameters: { + case: "${workflow.input.case}", + }, + type: "SWITCH", + decisionCases: { + "": [], + "CASE-2": [], + "CASE-1": [], + }, + defaultCase: [ + { + name: "http", + taskReferenceName: "http_ref", + inputParameters: { + method: "GET", + asyncComplete: false, + readTimeOut: "3000", + uri: "https://orkes-api-tester.orkesconductor.com/api", + connectionTimeOut: 3000, + contentType: "application/json", + accept: "application/json", + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + executionData: { + status: "COMPLETED", + executed: true, + attempts: 1, + outputData: { + response: { + headers: { + "Strict-Transport-Security": [ + "max-age=15724800; includeSubDomains", + ], + Connection: ["keep-alive"], + "Content-Length": ["182"], + Date: ["Fri, 12 Apr 2024 18:54:54 GMT"], + "Content-Type": ["application/json"], + }, + reasonPhrase: "OK", + body: { + randomInt: 2850, + hostName: "orkes-api-sampler-67dfc8cf58-lp2np", + randomString: "rvewnskyhuakqjctndpd", + queryParams: {}, + sleepFor: "0 ms", + apiRandomDelay: "0 ms", + statusCode: "200", + }, + statusCode: 200, + }, + }, + }, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + rateLimited: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + evaluatorType: "graaljs", + expression: "((\n function () {\n return $.case;\n }\n))();", + onStateChange: {}, + executionData: { + status: "COMPLETED", + executed: true, + attempts: 1, + outputData: { + evaluationResult: ["null"], + selectedCase: "null", + }, + }, +}; + +export const decisionExecutionDataWithValidCase = { + name: "decision_gateway", + taskReferenceName: "approval_decision", + inputParameters: { + case_value_param: "${inline_ref.output.result}", + }, + type: "DECISION", + caseValueParam: "case_value_param", + decisionCases: { + LOW: [ + { + name: "http", + taskReferenceName: "http_ref", + inputParameters: { + uri: "https://orkes-api-tester.orkesconductor.com/api", + method: "GET", + accept: "application/json", + contentType: "application/json", + encode: true, + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + permissive: false, + }, + ], + MEDIUM: [ + { + name: "http_1", + taskReferenceName: "http_ref_1", + inputParameters: { + uri: "https://orkes-api-tester.orkesconductor.com/api", + method: "GET", + accept: "application/json", + contentType: "application/json", + encode: true, + asyncComplete: false, + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + permissive: false, + }, + ], + HIGH: [ + { + name: "http_2", + taskReferenceName: "http_ref_2", + inputParameters: { + uri: "https://orkes-api-tester.orkesconductor.com/api", + method: "GET", + accept: "application/json", + contentType: "application/json", + encode: true, + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + permissive: false, + }, + ], + }, + defaultCase: [ + { + name: "http_3", + taskReferenceName: "http_ref_3", + inputParameters: { + uri: "https://orkes-api-tester.orkesconductor.com/api", + method: "GET", + accept: "application/json", + contentType: "application/json", + encode: true, + }, + type: "HTTP", + decisionCases: {}, + defaultCase: [], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + permissive: false, + }, + ], + forkTasks: [], + startDelay: 0, + joinOn: [], + optional: false, + defaultExclusiveJoinTask: [], + asyncComplete: false, + loopOver: [], + onStateChange: {}, + permissive: false, +}; diff --git a/ui-next/src/theme/index.ts b/ui-next/src/theme/index.ts new file mode 100644 index 0000000000..6f417ede7e --- /dev/null +++ b/ui-next/src/theme/index.ts @@ -0,0 +1,2 @@ +export { Provider as ThemeProvider } from "./material/provider"; +export { default as getTheme } from "./theme"; diff --git a/ui-next/src/theme/material/ColorModeContext/ColorModeContext.tsx b/ui-next/src/theme/material/ColorModeContext/ColorModeContext.tsx new file mode 100644 index 0000000000..504cb53767 --- /dev/null +++ b/ui-next/src/theme/material/ColorModeContext/ColorModeContext.tsx @@ -0,0 +1,13 @@ +import { PaletteMode } from "@mui/material"; +import { createContext } from "react"; + +interface ThemeProviderContext { + mode: PaletteMode; + toggler?: { + toggleColorMode: () => void; + }; +} + +export const ColorModeContext = createContext({ + mode: "light", +}); diff --git a/ui-next/src/theme/material/ColorModeContext/index.ts b/ui-next/src/theme/material/ColorModeContext/index.ts new file mode 100644 index 0000000000..ce48ba9103 --- /dev/null +++ b/ui-next/src/theme/material/ColorModeContext/index.ts @@ -0,0 +1 @@ +export * from "./ColorModeContext"; diff --git a/ui-next/src/theme/material/baseTheme.ts b/ui-next/src/theme/material/baseTheme.ts new file mode 100644 index 0000000000..31ac24cde3 --- /dev/null +++ b/ui-next/src/theme/material/baseTheme.ts @@ -0,0 +1,124 @@ +import darkScrollbar from "@mui/material/darkScrollbar"; +import { Theme, ThemeOptions, createTheme } from "@mui/material/styles"; +import _path from "lodash/fp/path"; +import { logger } from "utils/logger"; +import { + borders, + breakpoints, + fontFamily, + fontSizes, + fontWeights, + lineHeights, + spacings, +} from "../tokens/variables"; + +function toNumber(v: string): number { + return parseFloat(v); +} + +const spacingFn = (factor: string | number) => { + const unit = toNumber(spacings.space0); + + // Support theme.spacing('space3') + if (typeof factor === "string") { + const spacingFactor = _path(factor, spacings) as string; + if (!spacingFactor) { + logger.warn(`spacingFn: ${factor} is not a valid spacing factor`); + } + return toNumber(spacingFactor ?? "0"); + } + + if (typeof factor === "number") { + // Support theme.spacing(2) + return unit * factor; + } + + return unit; +}; + +const baseThemeOptions: ThemeOptions = { + typography: { + fontFamily: fontFamily.fontFamilySans, + fontSize: toNumber(fontSizes.fontSize2), + htmlFontSize: toNumber(fontSizes.fontSize2), + fontWeightLight: fontWeights.fontWeight0, + fontWeightRegular: fontWeights.fontWeight0, + fontWeightMedium: fontWeights.fontWeight1, + fontWeightBold: fontWeights.fontWeight2, + h1: { + fontSize: fontSizes.fontSize10, + lineHeight: lineHeights.lineHeight0, + fontWeight: fontWeights.fontWeight2, + }, + h2: { + fontSize: fontSizes.fontSize9, + lineHeight: lineHeights.lineHeight0, + fontWeight: fontWeights.fontWeight2, + }, + h3: { + fontSize: fontSizes.fontSize8, + lineHeight: lineHeights.lineHeight0, + fontWeight: fontWeights.fontWeight2, + }, + h4: { + fontSize: fontSizes.fontSize7, + lineHeight: lineHeights.lineHeight0, + fontWeight: fontWeights.fontWeight2, + }, + h5: { + fontSize: fontSizes.fontSize6, + lineHeight: lineHeights.lineHeight0, + fontWeight: fontWeights.fontWeight2, + }, + h6: { + fontSize: fontSizes.fontSize5, + lineHeight: lineHeights.lineHeight0, + fontWeight: fontWeights.fontWeight2, + }, + body1: { + fontSize: fontSizes.fontSize2, + lineHeight: lineHeights.lineHeight1, + }, + body2: { + fontSize: fontSizes.fontSize3, + lineHeight: lineHeights.lineHeight1, + }, + caption: { + fontSize: fontSizes.fontSize2, + lineHeight: lineHeights.lineHeight1, + }, + button: { + fontSize: fontSizes.fontSize2, + fontWeight: fontWeights.fontWeight1, + }, + }, + breakpoints: { + // this looks wrong, but it's not + // material's breakpoints are a range, so the below basically says + // xs is from 0 to breakpoints.large + values: { + xs: 0, + sm: toNumber(breakpoints.xsmall), + md: toNumber(breakpoints.small), + lg: toNumber(breakpoints.medium), + xl: toNumber(breakpoints.large), + // Breakpoint to display link buttons on navbar + }, + }, + shape: { + borderRadius: toNumber(borders.radiusSmall), + }, + //color: colorFn, + spacing: spacingFn, + components: { + MuiCssBaseline: { + styleOverrides: (themeParam: Theme) => ({ + body: themeParam.palette.mode === "dark" ? darkScrollbar() : null, + }), + }, + }, +}; + +const baseTheme = createTheme(baseThemeOptions); + +export default baseTheme; diff --git a/ui-next/src/theme/material/components/appBar.ts b/ui-next/src/theme/material/components/appBar.ts new file mode 100644 index 0000000000..e84584b823 --- /dev/null +++ b/ui-next/src/theme/material/components/appBar.ts @@ -0,0 +1,33 @@ +import { colors } from "../../tokens/variables"; +import { PaletteMode, Theme } from "@mui/material"; +import { Components } from "@mui/material/styles"; + +export const appBar = (mode: PaletteMode): Components => { + return { + MuiAppBar: { + styleOverrides: { + root: { + ...(mode === "light" + ? { + backgroundColor: colors.white, + color: colors.primary, + fontSize: "11pt !important", + fontWeight: 400, + } + : { + backgroundColor: colors.gray01, + color: colors.primary, + fontSize: "11pt !important", + fontWeight: 400, + }), + boxShadow: "none !important", + "& .MuiLink-underlineHover:hover": { + textDecoration: "none !important", + }, + }, + }, + }, + }; +}; + +export default appBar; diff --git a/ui-next/src/theme/material/components/atoms.ts b/ui-next/src/theme/material/components/atoms.ts new file mode 100644 index 0000000000..b0056bee15 --- /dev/null +++ b/ui-next/src/theme/material/components/atoms.ts @@ -0,0 +1,73 @@ +import { PaletteMode, Theme } from "@mui/material"; +import { Components } from "@mui/material/styles"; +import { + borders, + colors, + fontSizes, + fontWeights, +} from "../../tokens/variables"; +import baseTheme from "../baseTheme"; + +const atoms = (_mode: PaletteMode): Components => ({ + MuiAvatar: { + styleOverrides: { + root: { + fontSize: "2.4rem", + }, + }, + }, + MuiLink: { + styleOverrides: { + root: { + textDecoration: "none", + color: colors.primary, + //color: mode === "light" ? colors.primary : colors.primaryLighter, + }, + }, + }, + MuiSvgIcon: { + styleOverrides: { + root: { + fontSize: fontSizes.fontSize6, + }, + }, + }, + MuiChip: { + styleOverrides: { + root: { + borderRadius: borders.radiusSmall, + height: "24px", + fontSize: fontSizes.fontSize2, + fontWeight: fontWeights.fontWeight1, + }, + label: ({ theme }) => ({ + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(2), + lineHeight: "27px", + }), + deleteIcon: { + height: "100%", + padding: 3, + margin: 0, + backgroundColor: "rgba(5, 5, 5, 0.1)", + borderRadius: `0 ${borders.radiusSmall} ${borders.radiusSmall} 0`, + width: 24, + boxSizing: "border-box", + textAlign: "center", + fill: baseTheme.palette.common.white, + borderLeftWidth: 1, + borderLeftStyle: "solid", + borderLeftColor: "rgba(5, 5, 5, 0.1)", + }, + deleteIconColorPrimary: { + color: colors.white, + }, + colorSecondary: { + color: colors.white, + backgroundColor: colors.lime07, + }, + }, + }, +}); + +export default atoms; diff --git a/ui-next/src/theme/material/components/buttons.ts b/ui-next/src/theme/material/components/buttons.ts new file mode 100644 index 0000000000..748d946f54 --- /dev/null +++ b/ui-next/src/theme/material/components/buttons.ts @@ -0,0 +1,305 @@ +import { PaletteMode, Theme } from "@mui/material"; +import { Components } from "@mui/material/styles"; +import { colors } from "theme/tokens/variables"; + +const lightButton: Partial> = { + MuiButton: { + defaultProps: { + disableRipple: true, + variant: "contained", + color: "primary", + size: "medium", + }, + styleOverrides: { + root: { + textTransform: "none", + borderRadius: "6px", + transition: "none", + fontWeight: 500, + boxShadow: "none", + padding: "8px 12px 8px 12px", + border: "1px solid inherit", + }, + sizeSmall: { + minHeight: "28px", + height: "28px", + fontSize: "12px", + }, + sizeMedium: { + minHeight: "36px", + height: "36px", + fontSize: "14px", + fontWeight: 500, + }, + sizeLarge: { + minHeight: "50px", + height: "50px", + fontSize: "16px", + }, + contained: { + border: `1px solid ${colors.sidebarFaintGrey}`, + }, + containedPrimary: { + color: colors.white, + backgroundColor: colors.blueLightMode, + border: `1px solid ${colors.blueLightMode}`, + + ":hover": { + color: colors.white, + backgroundColor: colors.blueLightMode, + border: `1px solid ${colors.blueLightMode}`, + boxShadow: `3px 3px 0px 0px ${colors.primaryHoverBoxShadow}`, + }, + + ":active": { + color: colors.sidebarBlacky, + backgroundColor: colors.darkBlueLightMode, + borderColor: colors.darkBlueLightMode, + boxShadow: "none", + }, + + "&.Mui-disabled": { + color: colors.defaultModalBackdropColor, + backgroundColor: colors.sidebarBarelyPastWhite, + borderColor: colors.defaultModalBackdropColor, + }, + }, + outlinedPrimary: { + color: colors.sidebarBlacky, + backgroundColor: undefined, + border: `1px solid ${colors.blueLight}`, + }, + textPrimary: { + color: colors.blueLightMode, + ":hover": { + backgroundColor: "unset", + }, + }, + containedSecondary: { + color: colors.blueLightMode, + backgroundColor: colors.white, + border: `1px solid ${colors.blueLightMode}`, + + ":hover": { + color: colors.blueLightMode, + backgroundColor: colors.white, + border: `1px solid ${colors.blueLightMode}`, + boxShadow: `3px 3px 0px 0px ${colors.secondaryHoverBoxShadow}`, + }, + + ":active": { + color: colors.sidebarBlacky, + backgroundColor: colors.darkBlueLightMode, + borderColor: colors.darkBlueLightMode, + boxShadow: "none", + }, + + "&.Mui-disabled": { + color: colors.defaultModalBackdropColor, + backgroundColor: colors.sidebarBarelyPastWhite, + borderColor: colors.defaultModalBackdropColor, + }, + }, + outlinedSecondary: { + color: colors.sidebarGreyDark, + borderColor: colors.sidebarGreyDark, + }, + textSecondary: { + color: colors.sidebarGreyDark, + ":hover": { + backgroundColor: "unset", + }, + }, + // @ts-ignore + containedTertiary: { + color: colors.sidebarGrey, + backgroundColor: colors.white, + border: `1px solid ${colors.sidebarFaintGrey}`, + + ":hover": { + color: colors.greyBg, + backgroundColor: colors.white, + borderColor: colors.sidebarFaintGrey, + boxShadow: `3px 3px 0px 0px ${colors.tertiaryHoverBoxShadow}`, + }, + + ":active": { + color: colors.sidebarGreyDark, + backgroundColor: colors.darkBlueLightMode, + borderColor: colors.darkBlueLightMode, + boxShadow: "none", + }, + + "&.Mui-disabled": { + color: colors.defaultModalBackdropColor, + backgroundColor: colors.sidebarBarelyPastWhite, + borderColor: colors.defaultModalBackdropColor, + }, + }, + outlinedTertiary: { + color: colors.sidebarGrey, + border: `1px solid ${colors.sidebarGrey}`, + + ":hover": { + color: colors.greyBg, + borderColor: colors.sidebarFaintGrey, + }, + }, + textTertiary: { + color: colors.sidebarGrey, + }, + containedError: { + border: `1px solid ${colors.red07}`, + + ":hover": { + backgroundColor: colors.red07, + border: `1px solid ${colors.red07}`, + boxShadow: `3px 3px 0px 0px ${colors.primaryHoverBoxShadow}`, + }, + + ":active": { + color: colors.sidebarBlacky, + backgroundColor: colors.red07, + borderColor: colors.red07, + boxShadow: "none", + }, + + "&.Mui-disabled": { + color: colors.defaultModalBackdropColor, + backgroundColor: colors.sidebarBarelyPastWhite, + borderColor: colors.defaultModalBackdropColor, + }, + }, + }, + }, + MuiIconButton: { + styleOverrides: { + root: { + color: colors.gray04, + borderColor: colors.gray04, + "&.Mui-disabled": { + color: colors.gray08, + borderColor: colors.gray08, + }, + "&:hover": { + backgroundColor: undefined, + }, + }, + }, + }, +}; + +const darkButton: Partial> = { + MuiButton: { + defaultProps: { + disableRipple: true, + variant: "contained", + color: "primary", + size: "medium", + }, + styleOverrides: { + root: { + textTransform: "none", + borderRadius: "6px", + transition: "none", + fontWeight: 500, + boxShadow: "none", + padding: "8px", + + "&.Mui-disabled": { + border: "none", + color: colors.sidebarFaintGrey, + backgroundColor: colors.sidebarBarelyPastWhite, + }, + + ":after": { + content: '""', + position: "absolute", + zIndex: -1, + right: 0, + bottom: 0, + width: "100%", + height: "100%", + background: `${colors.blueBackground}`, + border: `1px solid ${colors.sidebarGreyDark}`, + borderRadius: "6px", + opacity: 0, + transition: "opacity 0.3s ease-in-out, transform 0.3s ease-in-out", + }, + + ":hover": { + color: colors.sidebarFaintGrey, + backgroundColor: colors.sidebarGreyDark, + border: `1px solid ${colors.sidebarGreyDark}`, + + ":after": { + opacity: 1, + right: -5, + bottom: -5, + }, + }, + }, + sizeSmall: { + minHeight: "28px", + height: "28px", + fontSize: "10pt", + }, + sizeMedium: { + minHeight: "36px", + height: "36px", + fontSize: "11pt", + fontWeight: 500, + }, + sizeLarge: { + minHeight: "50px", + height: "50px", + fontSize: "14pt", + }, + outlinedPrimary: {}, + outlinedSecondary: { + color: colors.gray12, + borderColor: colors.gray12, + "&.Mui-disabled": { + color: colors.gray05, + borderColor: colors.gray05, + }, + }, + contained: { + border: `1px solid ${colors.sidebarFaintGrey}`, + }, + containedPrimary: { + "&.Mui-disabled": { + color: colors.gray09, + backgroundColor: colors.gray05, + }, + }, + containedSecondary: { + "&.Mui-disabled": { + color: colors.gray05, + backgroundColor: colors.gray02, + }, + }, + }, + }, + MuiIconButton: { + styleOverrides: { + root: { + color: colors.gray12, + borderColor: colors.gray12, + "&.Mui-disabled": { + color: colors.gray05, + borderColor: colors.gray05, + }, + "&:hover": { + backgroundColor: colors.gray06, + }, + }, + }, + }, +}; + +const buttons = (mode: PaletteMode): Components => { + return mode === "dark" ? darkButton : lightButton; +}; + +export default buttons; diff --git a/ui-next/src/theme/material/components/buttonsGroup.ts b/ui-next/src/theme/material/components/buttonsGroup.ts new file mode 100644 index 0000000000..b0a448119c --- /dev/null +++ b/ui-next/src/theme/material/components/buttonsGroup.ts @@ -0,0 +1,218 @@ +import { PaletteMode, Theme } from "@mui/material"; +import { Components } from "@mui/material/styles"; +import { colors } from "theme/tokens/variables"; + +const lightButtonGroup: Partial> = { + MuiButtonGroup: { + defaultProps: { + disableRipple: true, + variant: "contained", + color: "primary", + size: "medium", + }, + styleOverrides: { + root: { + textTransform: "none", + borderRadius: "6px", + transition: "none", + fontWeight: 500, + boxShadow: "none", + "&.Mui-disabled": { + color: colors.defaultModalBackdropColor, + backgroundColor: colors.sidebarBarelyPastWhite, + borderColor: colors.defaultModalBackdropColor, + }, + }, + + groupedContainedPrimary: { + color: colors.white, + backgroundColor: colors.blueLightMode, + border: `1px solid ${colors.blueLightMode}`, + + ":not(:last-of-type)": { + border: `none`, + borderRight: "1px solid white", + }, + + ":hover": { + color: colors.white, + backgroundColor: colors.blueLightMode, + border: `1px solid ${colors.blueLightMode}`, + boxShadow: `3px 3px 0px 0px ${colors.primaryHoverBoxShadow}`, + ":not(:last-of-type)": { + border: `none`, + borderRight: "1px solid white", + }, + }, + + ":active": { + color: colors.sidebarBlacky, + backgroundColor: colors.darkBlueLightMode, + borderColor: colors.darkBlueLightMode, + boxShadow: "none", + }, + + "&.Mui-disabled": { + color: colors.defaultModalBackdropColor, + backgroundColor: colors.sidebarBarelyPastWhite, + borderColor: colors.defaultModalBackdropColor, + ":not(:last-of-type)": { + border: `1px solid ${colors.defaultModalBackdropColor}`, + }, + }, + }, + + groupedContainedSecondary: { + color: colors.blueLightMode, + backgroundColor: colors.white, + border: `1px solid ${colors.blueLightMode}`, + + ":hover": { + color: colors.blueLightMode, + backgroundColor: colors.white, + border: `1px solid ${colors.blueLightMode}`, + boxShadow: `3px 3px 0px 0px ${colors.secondaryHoverBoxShadow}`, + }, + + ":active": { + color: colors.sidebarBlacky, + backgroundColor: colors.darkBlueLightMode, + borderColor: colors.darkBlueLightMode, + boxShadow: "none", + }, + + ":not(:last-of-type)": { + border: `1px solid ${colors.blueLightMode}`, + }, + "&.Mui-disabled": { + color: colors.defaultModalBackdropColor, + backgroundColor: colors.sidebarBarelyPastWhite, + borderColor: colors.defaultModalBackdropColor, + ":not(:last-of-type)": { + border: `1px solid ${colors.defaultModalBackdropColor}`, + }, + }, + }, + // @ts-ignore + groupedContainedTertiary: { + color: colors.sidebarGrey, + backgroundColor: colors.white, + border: `1px solid ${colors.sidebarFaintGrey}`, + + ":not(:last-of-type)": { + border: `1px solid ${colors.sidebarFaintGrey}`, + }, + ":hover": { + color: colors.greyBg, + backgroundColor: colors.white, + borderColor: colors.sidebarFaintGrey, + boxShadow: `3px 3px 0px 0px ${colors.tertiaryHoverBoxShadow}`, + }, + + ":active": { + color: colors.sidebarGreyDark, + backgroundColor: colors.darkBlueLightMode, + borderColor: colors.darkBlueLightMode, + boxShadow: "none", + }, + + "&.Mui-disabled": { + color: colors.defaultModalBackdropColor, + backgroundColor: colors.sidebarBarelyPastWhite, + borderColor: colors.defaultModalBackdropColor, + ":not(:last-of-type)": { + border: `1px solid ${colors.defaultModalBackdropColor}`, + }, + }, + }, + }, + }, +}; + +const darkButtonGroup: Partial> = { + MuiButtonGroup: { + defaultProps: { + disableRipple: true, + variant: "contained", + color: "primary", + size: "medium", + }, + styleOverrides: { + root: { + textTransform: "none", + borderRadius: "6px", + transition: "none", + fontWeight: 500, + boxShadow: "none", + padding: "8px", + + "&.Mui-disabled": { + border: "none", + color: colors.sidebarFaintGrey, + backgroundColor: colors.sidebarBarelyPastWhite, + }, + }, + groupedContained: { + ":after": { + content: '""', + position: "absolute", + zIndex: -1, + right: 0, + bottom: 0, + width: "100%", + height: "100%", + background: `${colors.blueBackground}`, + border: `1px solid ${colors.sidebarGreyDark}`, + borderRadius: "6px", + opacity: 0, + transition: "opacity 0.3s ease-in-out, transform 0.3s ease-in-out", + }, + + ":hover": { + color: colors.sidebarFaintGrey, + backgroundColor: colors.sidebarGreyDark, + border: `1px solid ${colors.sidebarGreyDark}`, + + ":after": { + opacity: 1, + right: -5, + bottom: -5, + }, + }, + }, + + groupedContainedPrimary: { + "&.Mui-disabled": { + color: colors.gray09, + backgroundColor: colors.gray05, + }, + }, + + groupedContainedSecondary: { + "&.Mui-disabled": { + color: colors.gray05, + backgroundColor: colors.gray02, + }, + ":not(:last-of-type)": { + border: `1px solid ${colors.blueLightMode}`, + }, + }, + // @ts-ignore + groupedContainedTertiary: { + "&.Mui-disabled": { + color: colors.gray05, + backgroundColor: colors.gray02, + }, + ":not(:last-of-type)": { + border: `1px solid ${colors.sidebarFaintGrey}`, + }, + }, + }, + }, +}; + +const buttonsGroup = (mode: PaletteMode): Components => { + return mode === "dark" ? darkButtonGroup : lightButtonGroup; +}; + +export default buttonsGroup; diff --git a/ui-next/src/theme/material/components/dropdownsMenusPopovers.ts b/ui-next/src/theme/material/components/dropdownsMenusPopovers.ts new file mode 100644 index 0000000000..547385d32f --- /dev/null +++ b/ui-next/src/theme/material/components/dropdownsMenusPopovers.ts @@ -0,0 +1,84 @@ +import { fontSizes, lineHeights } from "../../tokens/variables"; +import { Theme } from "@mui/material"; +import { Components } from "@mui/material/styles"; + +const dropdownsMenusAndPopovers = (): Components => { + return { + MuiMenu: { + defaultProps: { + transitionDuration: 0, + elevation: 3, + }, + // styleOverrides: { + // dense: { + // paddingTop: 0, + // paddingBottom: 0, + // }, + // }, + }, + MuiPopover: { + defaultProps: { + elevation: 3, + }, + }, + MuiPopper: { + //@ts-ignore + styleOverrides: { + root: { + zIndex: 90000, + }, + }, + }, + MuiMenuItem: { + styleOverrides: { + root: { + fontSize: fontSizes.fontSize1, + }, + dense: { + paddingTop: 0, + paddingBottom: 0, + }, + }, + }, + MuiListItemText: { + styleOverrides: { + secondary: { + fontSize: fontSizes.fontSize1, + }, + primary: { + fontSize: fontSizes.fontSize1, + }, + }, + }, + MuiListSubheader: { + styleOverrides: { + root: ({ theme }) => ({ + fontSize: fontSizes.fontSize2, + lineHeight: lineHeights.lineHeight1, + paddingTop: theme.spacing(1), + paddingBottom: theme.spacing(1), + }), + }, + }, + MuiSnackbarContent: { + styleOverrides: { + root: ({ theme }) => ({ + backgroundColor: theme.palette.primary.main, + paddingTop: 0, + paddingBottom: 0, + marginRight: theme.spacing(4), + marginLeft: theme.spacing(4), + borderRadius: theme.shape.borderRadius, + boxShadow: "none", + }), + action: ({ theme }) => ({ + "& button": { + color: theme.palette.common.white, + }, + }), + }, + }, + }; +}; + +export default dropdownsMenusAndPopovers; diff --git a/ui-next/src/theme/material/components/formControls.ts b/ui-next/src/theme/material/components/formControls.ts new file mode 100644 index 0000000000..3f79446334 --- /dev/null +++ b/ui-next/src/theme/material/components/formControls.ts @@ -0,0 +1,218 @@ +import { PaletteMode, Theme } from "@mui/material"; +import { Components } from "@mui/material/styles"; + +import { colors, fontSizes } from "../../tokens/variables"; +import baseTheme from "../baseTheme"; + +export const SMALL_INPUT_HEIGHT = "36px"; + +export const inputLabelIdleStyles = {}; + +export const inputLabelFocusedStyles = { + color: colors.black, +}; + +const formControls = (mode: PaletteMode): Components => { + const darkMode = mode === "dark"; + + return { + MuiFormControl: { + defaultProps: { + size: "small", + }, + styleOverrides: { + root: { + display: "block", + }, + }, + }, + MuiInputBase: { + styleOverrides: { + root: { + fontSize: fontSizes.fontSize2, + }, + input: { + "&[type=number]::-webkit-inner-spin-button ": { + appearance: "none", + margin: 0, + }, + }, + sizeSmall: { + minHeight: SMALL_INPUT_HEIGHT, + }, + }, + }, + MuiTextField: { + defaultProps: { + variant: "outlined", + InputProps: { + notched: false, + }, + InputLabelProps: { + shrink: true, + }, + }, + }, + MuiCheckbox: { + defaultProps: { + size: "small", + }, + styleOverrides: { + root: ({ theme }) => ({ + fontSize: fontSizes.fontSize0, + padding: theme.spacing(2), + }), + colorSecondary: ({ theme }) => ({ + color: colors.blackLight, + "&$checked": { + color: theme.palette.primary.main, + }, + "&$disabled": { + color: colors.blackXLight, + }, + }), + }, + }, + MuiSwitch: { + styleOverrides: { + root: { + padding: 0, + marginRight: 8, + marginLeft: 8, + height: 20, + width: 40, + "&:hover": { + "& > $track": { + backgroundColor: colors.gray05, + }, + "& > $checked + $track": { + backgroundColor: colors.brand05, + }, + }, + }, + thumb: { + borderRadius: 8, + width: 16, + height: 16, + color: "white", + boxShadow: + "0px 1px 2px 0px rgba(0, 0, 0, 0.4), 0px 0px 1px 0px rgba(0, 0, 0, 0.4)", + }, + track: ({ theme }) => ({ + backgroundColor: colors.gray07, + borderRadius: 10, + opacity: 1, + ".Mui-checked.Mui-checked + &": { + // track - checked + backgroundColor: theme.palette.green.primary, + opacity: 1, + }, + }), + switchBase: { + padding: 2, + "&$checked": { + // transform: "translateX(100%)", + "& + $track": { + opacity: 1, + }, + }, + }, + colorPrimary: ({ theme }) => ({ + "&$checked": { + color: theme.palette.common.white, + }, + "&$checked + $track": { + backgroundColor: theme.palette.primary.main, + }, + }), + }, + }, + MuiRadio: { + styleOverrides: { + root: ({ theme }) => ({ + padding: theme.spacing(2), + }), + }, + }, + MuiOutlinedInput: {}, + MuiFormControlLabel: { + styleOverrides: { + root: { + marginLeft: -8, + }, + }, + }, + MuiInputLabel: { + defaultProps: { + shrink: true, + }, + styleOverrides: { + root: { + pointerEvents: "auto", + color: baseTheme.palette.text.primary, + "&.MuiInputLabel-outlined": { + "&.MuiInputLabel-focused": inputLabelFocusedStyles, + }, + }, + }, + }, + MuiFormHelperText: { + styleOverrides: { + contained: ({ theme }) => ({ + margin: 0, + marginTop: theme.spacing(2), + }), + }, + }, + MuiSelect: { + styleOverrides: { + icon: { + fontSize: fontSizes.fontSize5, + color: + mode === "dark" ? colors.gray12 : baseTheme.palette.text.primary, + }, + }, + }, + MuiAutocomplete: { + defaultProps: { + componentsProps: { + paper: { + elevation: 3, + }, + }, + }, + styleOverrides: { + paper: { + fontSize: fontSizes.fontSize2, + boxShadow: `0 0 10px ${ + darkMode ? colors.gray08 : "rgba(0, 0, 0, .3)" + }`, + }, + popupIndicator: { + fontSize: fontSizes.fontSize5, + color: baseTheme.palette.text.primary, + }, + clearIndicator: { + fontSize: fontSizes.fontSize5, + color: darkMode ? colors.gray12 : baseTheme.palette.text.primary, + }, + inputRoot: ({ theme }) => ({ + paddingLeft: theme.spacing(3), + paddingRight: theme.spacing(3), + }), + tag: { + "&:first-of-type": { + marginLeft: 8, + }, + }, + option: { + "&.MuiAutocomplete-option.Mui-focused": { + backgroundColor: darkMode ? colors.blue04 : colors.blue13, + }, + }, + }, + }, + }; +}; + +export default formControls; diff --git a/ui-next/src/theme/material/components/modals.ts b/ui-next/src/theme/material/components/modals.ts new file mode 100644 index 0000000000..5b75023e93 --- /dev/null +++ b/ui-next/src/theme/material/components/modals.ts @@ -0,0 +1,46 @@ +import { colors } from "../../tokens/variables"; +import { PaletteMode, Theme } from "@mui/material"; +import { Components } from "@mui/material/styles"; + +const modals = (mode: PaletteMode): Components => { + return { + MuiDialog: { + styleOverrides: { + paper: ({ theme }) => ({ + borderRadius: theme.spacing(3), + boxShadow: theme.shadows[mode === "dark" ? 24 : 16], + }), + }, + }, + MuiDialogContent: { + styleOverrides: { + root: ({ theme }) => ({ + padding: theme.spacing(6), + }), + }, + }, + MuiDialogActions: { + styleOverrides: { + root: ({ theme }) => ({ + padding: `${theme.spacing(4)} ${theme.spacing(6)}`, + background: colors.blackXXLight, + borderTop: `1px solid ${colors.blackXXLight}`, + margin: 0, + gap: `${theme.spacing(4)}`, + }), + }, + }, + MuiDialogTitle: { + styleOverrides: { + root: ({ theme }) => ({ + fontSize: theme.typography.pxToRem(14), + padding: `${theme.spacing(6)}px ${theme.spacing(5)}px`, + background: colors.blackXXLight, + borderBottom: `1px solid ${colors.blackXXLight}`, + }), + }, + }, + }; +}; + +export default modals; diff --git a/ui-next/src/theme/material/components/paper.ts b/ui-next/src/theme/material/components/paper.ts new file mode 100644 index 0000000000..592c22f902 --- /dev/null +++ b/ui-next/src/theme/material/components/paper.ts @@ -0,0 +1,12 @@ +const paper = { + MuiPaper: { + defaultProps: { + elevation: 0, + }, + styleOverrides: { + borderRadius: "4px", + }, + }, +}; + +export default paper; diff --git a/ui-next/src/theme/material/components/tables.ts b/ui-next/src/theme/material/components/tables.ts new file mode 100644 index 0000000000..fdb359f122 --- /dev/null +++ b/ui-next/src/theme/material/components/tables.ts @@ -0,0 +1,40 @@ +import { fontSizes, fontWeights, colors } from "../../tokens/variables"; + +const tables = { + MuiTablePagination: { + styleOverrides: { + select: { + paddingRight: "32px !important", + }, + selectRoot: { + top: 1, + }, + }, + }, + MuiTableCell: { + styleOverrides: { + root: { + fontSize: fontSizes.fontSize2, + }, + head: { + //border: 'none', + fontWeight: fontWeights.fontWeight1, + color: colors.gray05, + }, + }, + }, + MuiTableRow: { + styleOverrides: { + root: { + "&.Mui-selected:hover": { + backgroundColor: colors.gray12, + }, + "&.Mui-selected": { + backgroundColor: `${colors.gray12} !important`, + }, + }, + }, + }, +}; + +export default tables; diff --git a/ui-next/src/theme/material/components/tabs.ts b/ui-next/src/theme/material/components/tabs.ts new file mode 100644 index 0000000000..07f19e32b2 --- /dev/null +++ b/ui-next/src/theme/material/components/tabs.ts @@ -0,0 +1,36 @@ +import { colors, fontSizes } from "../../tokens/variables"; +import { PaletteMode } from "@mui/material"; + +const tabs = (mode: PaletteMode) => { + const darkMode = mode === "dark"; + + return { + MuiTabs: { + styleOverrides: { + indicator: { + height: 2, + }, + scroller: { + backgroundColor: darkMode ? colors.black : colors.white, + }, + }, + }, + MuiTab: { + defaultProps: { + disableRipple: true, + }, + styleOverrides: { + root: { + textTransform: "none", + color: darkMode ? colors.gray08 : undefined, + "&.Mui-selected": { + color: darkMode ? colors.gray14 : colors.gray00, + }, + fontSize: fontSizes.fontSize2, + }, + }, + }, + }; +}; + +export default tabs; diff --git a/ui-next/src/theme/material/getPaletteForMode.ts b/ui-next/src/theme/material/getPaletteForMode.ts new file mode 100644 index 0000000000..1b368254a9 --- /dev/null +++ b/ui-next/src/theme/material/getPaletteForMode.ts @@ -0,0 +1,221 @@ +import { Palette, PaletteMode, PaletteOptions } from "@mui/material"; +import { ThemeOptions } from "@mui/material/styles"; +import { colors } from "theme/tokens/variables"; +import { FEATURES, featureFlags } from "utils/flags"; + +// TODO: get rid of these components after applying new inputs whole app +const enabledWhiteBackgroundForm = featureFlags.isEnabled( + FEATURES.ENABLE_WHITE_BACKGROUND_FORM, +); + +export const lightModePalette: Partial = { + primary: { + main: colors.primary, + light: colors.bgBrandLight, + dark: colors.bgBrandDark, + contrastText: colors.white, + }, + success: { + main: colors.successTag, + light: colors.green08, + dark: colors.green04, + contrastText: colors.white, + }, + warning: { + main: colors.orange07, + light: colors.orange08, + dark: colors.orange06, + contrastText: colors.white, + }, + secondary: { + main: colors.gray12, + light: colors.gray14, + dark: colors.gray10, + contrastText: colors.gray00, + }, + text: { + primary: colors.black, + secondary: colors.blackXLight, + disabled: colors.blackXXLight, + hint: colors.blackXXLight, + }, + grey: { + 50: colors.gray14, + 100: colors.gray13, + 200: colors.gray12, + 300: colors.gray11, + 400: colors.gray10, + 500: colors.gray09, + 600: colors.gray07, + 700: colors.gray06, + 800: colors.gray04, + 900: colors.gray02, + A100: colors.gray12, + A200: colors.gray08, + A400: colors.gray03, + A700: colors.gray06, + }, + error: { + main: colors.failure, + light: colors.failureLight, + dark: colors.failureDark, + contrastText: colors.white, + }, + background: { + paper: colors.white, + default: colors.gray14, + }, + divider: colors.blackXXLight, + // Custom from here + purple: { + main: colors.purple, + light: colors.lightPurple, + }, + pink: { + main: colors.errorTag, + }, + faintGrey: colors.sidebarFaintGrey, + tertiary: { + main: colors.white, + light: colors.white, + dark: colors.white, + contrastText: colors.sidebarGrey, + }, + blue: { + main: colors.blueLight, + light: colors.blueLightMode, + dark: colors.blueLightMode, + contrastText: colors.blueLightMode, + }, + input: { + text: colors.sidebarBlacky, + label: colors.sidebarUIVersion, + border: colors.greyText2, + focus: colors.blueLightMode, + error: colors.errorRed, + background: colors.white, + disabled: colors.greyText, + }, + label: { + text: colors.sidebarGrey, + disabled: colors.greyText, + }, + green: { + primary: colors.primaryGreen, + }, + customBackground: { + main: colors.sidebarBarelyPastWhite, + form: enabledWhiteBackgroundForm ? colors.white : colors.gray14, + }, +}; + +export const darkModePalette: Partial = { + // Dark mode colors + primary: { + main: colors.primary, + light: colors.bgBrandLight, + dark: colors.bgBrandDark, + contrastText: colors.white, + }, + success: { + main: colors.green06, + light: colors.green06, + dark: colors.green06, + contrastText: colors.white, + }, + warning: { + main: colors.orange06, + light: colors.orange06, + dark: colors.orange06, + contrastText: colors.white, + }, + secondary: { + main: colors.gray04, + light: colors.gray06, + dark: colors.gray08, + contrastText: colors.gray14, + }, + text: { + primary: colors.gray14, + secondary: colors.gray12, + disabled: colors.gray09, + hint: colors.gray09, + }, + grey: { + 50: colors.gray14, + 100: colors.gray13, + 200: colors.gray12, + 300: colors.gray11, + 400: colors.gray10, + 500: colors.gray09, + 600: colors.gray07, + 700: colors.gray06, + 800: colors.gray04, + 900: colors.gray02, + A100: colors.gray12, + A200: colors.gray08, + A400: colors.gray03, + A700: colors.gray06, + }, + error: { + main: colors.failure, + light: colors.failureLight, + dark: colors.failureDark, + contrastText: colors.white, + }, + background: { + paper: colors.gray01, + default: colors.black, + }, + divider: colors.blackXXLight, + // Custom from here + purple: { + main: colors.purple, + light: colors.lightPurple, + }, + pink: { + main: colors.errorTag, + }, + faintGrey: colors.sidebarFaintGrey, + tertiary: { + main: colors.white, + light: colors.white, + dark: colors.white, + contrastText: colors.sidebarGrey, + }, + blue: { + main: colors.blueLight, + light: colors.blueLightMode, + dark: colors.blueLightMode, + contrastText: colors.blueLightMode, + }, + input: { + text: colors.sidebarBlacky, + label: colors.sidebarGrey, + border: colors.greyText2, + focus: colors.blueLight, + error: colors.errorRed, + background: colors.white, + disabled: colors.greyText, + }, + label: { + text: colors.sidebarGrey, + disabled: colors.greyText, + }, + green: { + primary: colors.primaryGreen, + }, + customBackground: { + main: colors.sidebarBarelyPastWhite, + form: enabledWhiteBackgroundForm ? colors.white : colors.gray00, + }, +}; + +export const getPaletteForMode = (mode: PaletteMode): ThemeOptions => { + return { + palette: { + mode, + ...(mode === "light" ? lightModePalette : darkModePalette), + }, + }; +}; diff --git a/ui-next/src/theme/material/provider.tsx b/ui-next/src/theme/material/provider.tsx new file mode 100644 index 0000000000..d6f9676704 --- /dev/null +++ b/ui-next/src/theme/material/provider.tsx @@ -0,0 +1,33 @@ +import { PaletteMode } from "@mui/material"; +import { ThemeProvider } from "@mui/material/styles"; +import { FunctionComponent, ReactNode, useMemo, useState } from "react"; +import { getTheme } from "../theme"; +import { ColorModeContext } from "./ColorModeContext/ColorModeContext"; + +export const Provider: FunctionComponent<{ children: ReactNode }> = ({ + children, + ...rest +}) => { + const [mode, setMode] = useState("light"); + const toggler = useMemo( + () => ({ + toggleColorMode: () => { + setMode((prevMode) => (prevMode === "light" ? "dark" : "light")); + }, + }), + [], + ); + + // Update the theme only if the mode changes + const lightOrDarkTheme = useMemo(() => { + return getTheme(mode); + }, [mode]); + + return ( + + + {children} + + + ); +}; diff --git a/ui-next/src/theme/material/types/Palette.d.ts b/ui-next/src/theme/material/types/Palette.d.ts new file mode 100644 index 0000000000..2a3bd10674 --- /dev/null +++ b/ui-next/src/theme/material/types/Palette.d.ts @@ -0,0 +1,69 @@ +import "@mui/material/styles"; + +interface CustomPalette { + purple: { + main: string; + light: string; + }; + pink: { + main: string; + }; + faintGrey: string; + tertiary: { + main: string; + light: string; + dark: string; + contrastText: string; + }; + blue: { + main: string; + light: string; + dark: string; + contrastText: string; + }; + input: { + text: string; + label: string; + border: string; + focus: string; + error: string; + background: string; + disabled: string; + }; + label: { + text: string; + disabled: string; + }; + green: { + primary: string; + }; + customBackground: { + main: string; + form: string; + }; +} + +declare module "@mui/material/styles" { + interface TypeText { + hint: string; + } + interface TypeTextOptions { + hint?: string; + } + + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + interface Palette extends CustomPalette {} + + interface PaletteOptions { + text?: Partial; + input?: Partial; + purple?: Partial; + pink?: Partial; + faintGrey?: string; + tertiary?: Partial; + blue?: Partial; + label?: Partial; + green?: Partial; + customBackground?: Partial; + } +} diff --git a/ui-next/src/theme/theme.ts b/ui-next/src/theme/theme.ts new file mode 100644 index 0000000000..c35e7d4a03 --- /dev/null +++ b/ui-next/src/theme/theme.ts @@ -0,0 +1,52 @@ +import baseTheme from "./material/baseTheme"; +import appBar from "./material/components/appBar"; +import paper from "./material/components/paper"; +import atoms from "./material/components/atoms"; +import buttons from "./material/components/buttons"; +import formControls from "./material/components/formControls"; +import dropdownsMenusPopovers from "./material/components/dropdownsMenusPopovers"; +import modals from "./material/components/modals"; +import tables from "./material/components/tables"; +import tabs from "./material/components/tabs"; + +import { PaletteMode } from "@mui/material"; + +import { ThemeOptions, createTheme } from "@mui/material/styles"; +import { getPaletteForMode } from "./material/getPaletteForMode"; +import buttonsGroup from "./material/components/buttonsGroup"; + +export const getOverridesForMode = (mode: PaletteMode) => { + const overrides = { + components: { + ...appBar(mode), + ...paper, + // the tiniest reusables like Chip, Link, SvgIcon, etc. + ...atoms(mode), + // ALL buttons + ...buttons(mode), + // button group + ...buttonsGroup(mode), + // inputs, checkboxes, radios, textareas, autocomplete, etc. + ...formControls(mode), + // all kinds of popovers, dropdowns, toasts, snackbars, + ...dropdownsMenusPopovers(), + ...modals(mode), + ...tables, + ...tabs(mode), + }, + }; + + return overrides as ThemeOptions; +}; + +export const getTheme = (mode: PaletteMode = "light") => { + return createTheme( + baseTheme, + getOverridesForMode(mode), + getPaletteForMode(mode), + ); +}; + +export default getTheme; + +export const LOCAL_STORAGE_DARK_MODE_TOGGLE_KEY = "dark-mode-toggle"; diff --git a/ui-next/src/theme/tokens/colorOverrides.ts b/ui-next/src/theme/tokens/colorOverrides.ts new file mode 100644 index 0000000000..9c19539788 --- /dev/null +++ b/ui-next/src/theme/tokens/colorOverrides.ts @@ -0,0 +1,41 @@ +import * as colors from "./colors"; + +const brandAliases = { + brand00: colors.indigo00, + brand01: colors.indigo01, + brand02: colors.indigo02, + brand03: colors.indigo03, + brand04: colors.indigo04, + brand05: colors.indigo05, + brand06: colors.indigo06, + brand07: colors.indigo07, + brand08: colors.indigo08, + brand09: colors.indigo09, + brand10: colors.indigo10, + brand11: colors.indigo11, + brand12: colors.indigo12, + brand13: colors.indigo13, + brand14: colors.indigo14, +}; + +const brandShortcuts = { + brand: brandAliases.brand07, + bgBrand: brandAliases.brand07, + bgBrandLight: brandAliases.brand09, + bgBrandDark: brandAliases.brand05, + brandXLight: colors.indigoXLight, + brandXXLight: colors.indigoXXLight, +}; + +const failureAliases = { + failure: colors.red07, + failureLight: colors.red09, + failureDark: colors.red05, +}; + +export const colorOverrides = { + ...colors, + ...brandAliases, + ...brandShortcuts, + ...failureAliases, +}; diff --git a/ui-next/src/theme/tokens/colors.js b/ui-next/src/theme/tokens/colors.js new file mode 100644 index 0000000000..877c906c04 --- /dev/null +++ b/ui-next/src/theme/tokens/colors.js @@ -0,0 +1,838 @@ +// Backgrounds / Black +export const black = "#050505"; + +// Transparents / Black / 00-Black-Light (70%) +export const blackLight = "rgba(5,5,5,0.7)"; + +// Transparents / Black / 01-Black-Xlight (40%) +export const blackXLight = "rgba(5,5,5,0.4)"; + +// Transparents / Black / 02-Black-Xxlight (10%) +export const blackXXLight = "rgba(5,5,5,0.1)"; + +// Backgrounds / Blue / Blue-00 (Xxdark) +export const blue00 = "#00101f"; + +// Backgrounds / Blue / Blue-01 +export const blue01 = "#05192b"; + +// Backgrounds / Blue / Blue-02 +export const blue02 = "#092743"; + +// Backgrounds / Blue / Blue-03 (Xdark) +export const blue03 = "#0d365c"; + +// Backgrounds / Blue / Blue-04 +export const blue04 = "#12487a"; + +// Backgrounds / Blue / Blue-05 (Dark) +export const blue05 = "#165b99"; + +// Backgrounds / Blue / Blue-06 +export const blue06 = "#1b6fb9"; + +// Backgrounds / Blue / -Blue-07 (Base) +export const blue07 = "#1f83db"; + +// Backgrounds / Blue / Blue-08 +export const blue08 = "#5995e1"; + +// Backgrounds / Blue / Blue-09 (Light) +export const blue09 = "#7ea7e7"; + +// Backgrounds / Blue / Blue-10 +export const blue10 = "#9dbaec"; + +// Backgrounds / Blue / Blue-11 (Xlight) +export const blue11 = "#bacdf2"; + +// Backgrounds / Blue / Blue-12 +export const blue12 = "#d2def6"; + +// Backgrounds / Blue / Blue-13 +export const blue13 = "#eaf0fb"; + +// Backgrounds / Blue / Blue-14 (Xxlight) +export const blue14 = "#f7fafd"; + +// Backgrounds / Blue / Blue-15 +export const blue15 = "#1976D2"; + +// Transparents / Blue / 00-Blue-Light (70%) +// export const blueLight = "rgba(31,131,219,0.7)"; + +// Transparents / Blue / 01-Blue-Xlight (40%) +export const blueXLight = "rgba(31,131,219,0.4)"; + +// Transparents / Blue / 02-Blue-Xxlight (10%) +export const blueXXLight = "rgba(31,131,219,0.1)"; + +// Backgrounds / Cyan / Cyan-00 (Xxdark) +export const cyan00 = "#001b1e"; + +// Backgrounds / Cyan / Cyan-01 +export const cyan01 = "#042529"; + +// Backgrounds / Cyan / Cyan-02 +export const cyan02 = "#08373d"; + +// Backgrounds / Cyan / Cyan-03 (Xdark) +export const cyan03 = "#0f4a52"; + +// Backgrounds / Cyan / Cyan-04 +export const cyan04 = "#17616c"; + +// Backgrounds / Cyan / Cyan-05 (Dark) +export const cyan05 = "#207986"; + +// Backgrounds / Cyan / Cyan-06 +export const cyan06 = "#2991a2"; + +// Backgrounds / Cyan / -Cyan-07 (Base) +export const cyan07 = "#32abbe"; + +// Backgrounds / Cyan / Cyan-08 +export const cyan08 = "#5fb8c8"; + +// Backgrounds / Cyan / Cyan-09 (Light) +export const cyan09 = "#80c5d2"; + +// Backgrounds / Cyan / Cyan-10 +export const cyan10 = "#9ed2dc"; + +// Backgrounds / Cyan / Cyan-11 (Xlight) +export const cyan11 = "#badfe6"; + +// Backgrounds / Cyan / Cyan-12 +export const cyan12 = "#d2eaef"; + +// Backgrounds / Cyan / Cyan-13 +export const cyan13 = "#eaf5f8"; + +// Backgrounds / Cyan / Cyan-14 (Xxlight) +export const cyan14 = "#f7fcfd"; + +// Transparents / Cyan / 00-Cyan-Light (70%) +export const cyanLight = "rgba(50,171,190,0.7)"; + +// Transparents / Cyan / 01-Cyan-Xlight (40%) +export const cyanXLight = "rgba(50,171,190,0.4)"; + +// Transparents / Cyan / 02-Cyan-Xxlight (10%) +export const cyanXXLight = "rgba(50,171,190,0.1)"; + +// Backgrounds / Grape / Grape-00 (Xxdark) +export const grape00 = "#18001f"; + +// Backgrounds / Grape / Grape-01 +export const grape01 = "#200b2a"; + +// Backgrounds / Grape / Grape-02 +export const grape02 = "#33143f"; + +// Backgrounds / Grape / Grape-03 (Xdark) +export const grape03 = "#481d56"; + +// Backgrounds / Grape / Grape-04 +export const grape04 = "#602871"; + +// Backgrounds / Grape / Grape-05 (Dark) +export const grape05 = "#7a338d"; + +// Backgrounds / Grape / Grape-06 +export const grape06 = "#943eab"; + +// Backgrounds / Grape / -Grape-07 (Base) +export const grape07 = "#b04ac9"; + +// Backgrounds / Grape / Grape-08 +export const grape08 = "#be68d2"; + +// Backgrounds / Grape / Grape-09 (Light) +export const grape09 = "#cb84da"; + +// Backgrounds / Grape / Grape-10 +export const grape10 = "#d89fe3"; + +// Backgrounds / Grape / Grape-11 (Xlight) +export const grape11 = "#e4baeb"; + +// Backgrounds / Grape / Grape-12 +export const grape12 = "#edd2f2"; + +// Backgrounds / Grape / Grape-13 +export const grape13 = "#f7e9f9"; + +// Backgrounds / Grape / Grape-14 (Xxlight) +export const grape14 = "#fcf7fd"; + +// Transparents / Grape / 00-Grape-Light (70%) +export const grapeLight = "rgba(176,74,201,0.7)"; + +// Transparents / Grape / 01-Grape-Xlight (40%) +export const grapeXLight = "rgba(176,74,201,0.4)"; + +// Transparents / Grape / 02-Grape-Xxlight (10%) +export const grapeXXLight = "rgba(176,74,201,0.1)"; + +// Backgrounds / Gray / Gray-00 (Xxdark) +export const gray00 = "#0f0f0f"; + +// Backgrounds / Gray / Gray-01 +export const gray01 = "#181818"; + +// Backgrounds / Gray / Gray-02 +export const gray02 = "#242424"; + +// Backgrounds / Gray / Gray-03 (Xdark) +export const gray03 = "#323232"; + +// Backgrounds / Gray / Gray-04 +export const gray04 = "#424242"; + +// Backgrounds / Gray / Gray-05 (Dark) +export const gray05 = "#535353"; + +// Backgrounds / Gray / Gray-06 +export const gray06 = "#646464"; + +// Backgrounds / Gray / -Gray-07 (Base) +export const gray07 = "#767676"; + +// Backgrounds / Gray / Gray-08 +export const gray08 = "#8a8a8a"; + +// Backgrounds / Gray / Gray-09 (Light) +export const gray09 = "#9e9e9e"; + +// Backgrounds / Gray / Gray-10 +export const gray10 = "#b3b3b3"; + +// Backgrounds / Gray / Gray-11 (Xlight) +export const gray11 = "#c8c8c8"; + +// Backgrounds / Gray / Gray-12 +export const gray12 = "#dbdbdb"; + +// Backgrounds / Gray / Gray-13 +export const gray13 = "#efefef"; + +// Backgrounds / Gray / Gray-14 (Xxlight) +export const gray14 = "#f3f3f3"; + +// Table background dark color +export const grayTableBackground = "#111111"; + +// Transparents / Gray / 00-Gray-Light (70%) +export const grayLight = "rgba(118,118,118,0.7)"; + +export const backgroundLeftTop = "ghostwhite"; + +// Transparents / Gray / 01-Gray-Xlight (40%) +export const grayXLight = "rgba(118,118,118,0.4)"; + +// Transparents / Gray / 02-Gray-Xxlight (10%) +export const grayXXLight = "rgba(118,118,118,0.1)"; + +// medium light shade of gray +export const lightShadesGray = "#aaa"; + +// Backgrounds / Green / Green-00 (Xxdark) +export const green00 = "#121e00"; + +// Backgrounds / Green / Green-01 +export const green01 = "#192a07"; + +// Backgrounds / Green / Green-02 +export const green02 = "#28400f"; + +// Backgrounds / Green / Green-03 (Xdark) +export const green03 = "#385714"; + +// Backgrounds / Green / Green-04 +export const green04 = "#4c731a"; + +// Backgrounds / Green / Green-05 (Dark) +export const green05 = "#61911f"; + +// Backgrounds / Green / Green-06 +export const green06 = "#76af25"; + +// Backgrounds / Green / -Green-07 (Base) +export const green07 = "#8ccf2a"; + +// Backgrounds / Green / Green-08 +export const green08 = "#a1d753"; + +// Backgrounds / Green / Green-09 (Light) +export const green09 = "#b4de74"; + +// Backgrounds / Green / Green-10 +export const green10 = "#c6e593"; + +// Backgrounds / Green / Green-11 (Xlight) +export const green11 = "#d7edb2"; + +// Backgrounds / Green / Green-12 +export const green12 = "#e5f3cd"; + +// Backgrounds / Green / Green-13 +export const green13 = "#f3f9e8"; + +// Backgrounds / Green / Green-14 (Xxlight) +export const green14 = "#fbfdf7"; + +// Transparents / Green / 00-Green-Light (70%) +export const greenLight = "rgba(140,207,42,0.7)"; + +// Transparents / Green / 01-Green-Xlight (40%) +export const greenXLight = "rgba(140,207,42,0.4)"; + +// Transparents / Green / 02-Green-Xxlight (10%) +export const greenXXLight = "rgba(140,207,42,0.1)"; + +// Backgrounds / Indigo / Indigo-00 (Xxdark) +export const indigo00 = "#00071f"; + +// Backgrounds / Indigo / Indigo-01 +export const indigo01 = "#07122c"; + +// Backgrounds / Indigo / Indigo-02 +export const indigo02 = "#0f1e44"; + +// Backgrounds / Indigo / Indigo-03 (Xdark) +export const indigo03 = "#192b5e"; + +// Backgrounds / Indigo / Indigo-04 +export const indigo04 = "#24397e"; + +// Backgrounds / Indigo / Indigo-05 (Dark) +export const indigo05 = "#30499f"; + +// Backgrounds / Indigo / Indigo-06 +export const indigo06 = "#3c59c1"; + +// Backgrounds / Indigo / -Indigo-07 (Base) +export const indigo07 = "#4969e4"; + +// Backgrounds / Indigo / Indigo-08 +export const indigo08 = "#6f7ee9"; + +// Backgrounds / Indigo / Indigo-09 (Light) +export const indigo09 = "#8e94ed"; + +// Backgrounds / Indigo / Indigo-10 +export const indigo10 = "#a9abf1"; + +// Backgrounds / Indigo / Indigo-11 (Xlight) +export const indigo11 = "#c2c2f5"; + +// Backgrounds / Indigo / Indigo-12 +export const indigo12 = "#d7d7f8"; + +// Backgrounds / Indigo / Indigo-13 +export const indigo13 = "#ebedfb"; + +// Backgrounds / Indigo / Indigo-14 (Xxlight) +export const indigo14 = "#f7f9fd"; + +// Transparents / Indigo / 00-Indigo-Light (70%) +export const indigoLight = "rgba(73,105,228,0.7)"; + +// Transparents / Indigo / 01-Indigo-Xlight (40%) +export const indigoXLight = "rgba(73,105,228,0.4)"; + +// Transparents / Indigo / 02-Indigo-Xxlight (10%) +export const indigoXXLight = "rgba(73,105,228,0.1)"; + +// Backgrounds / Lime / Lime-00 (Xxdark) +export const lime00 = "#001f06"; + +// Backgrounds / Lime / Lime-01 +export const lime01 = "#05290f"; + +// Backgrounds / Lime / Lime-02 +export const lime02 = "#0c3c19"; + +// Backgrounds / Lime / Lime-03 (Xdark) +export const lime03 = "#145124"; + +// Backgrounds / Lime / Lime-04 +export const lime04 = "#1f6930"; + +// Backgrounds / Lime / Lime-05 (Dark) +export const lime05 = "#2a833c"; + +// Backgrounds / Lime / Lime-06 +export const lime06 = "#359e4a"; + +// Backgrounds / Lime / -Lime-07 (Base) +export const lime07 = "#41b957"; + +// Backgrounds / Lime / Lime-08 +export const lime08 = "#65c470"; + +// Backgrounds / Lime / Lime-09 (Light) +export const lime09 = "#84d08a"; + +// Backgrounds / Lime / Lime-10 +export const lime10 = "#a0dba3"; + +// Backgrounds / Lime / Lime-11 (Xlight) +export const lime11 = "#bbe5bd"; + +// Backgrounds / Lime / Lime-12 +export const lime12 = "#d2efd4"; + +// Backgrounds / Lime / Lime-13 +export const lime13 = "#e9f8eb"; + +// Backgrounds / Lime / Lime-14 (Xxlight) +export const lime14 = "#f6fdf8"; + +// Transparents / Lime / 00-Lime-Light (70%) +export const limeLight = "rgba(65,185,87,0.7)"; + +// Transparents / Lime / 01-Lime-Xlight (40%) +export const limeXLight = "rgba(65,185,87,0.4)"; + +// Transparents / Lime / 02-Lime-Xxlight (10%) +export const limeXXLight = "rgba(65,185,87,0.1)"; + +// Backgrounds / Orange / Orange-00 (Xxdark) +export const orange00 = "#1e0c00"; + +// Backgrounds / Orange / Orange-01 +export const orange01 = "#2b1505"; + +// Backgrounds / Orange / Orange-02 +export const orange02 = "#46210d"; + +// Backgrounds / Orange / Orange-03 (Xdark) +export const orange03 = "#622e10"; + +// Backgrounds / Orange / Orange-04 +export const orange04 = "#853d12"; + +// Backgrounds / Orange / Orange-05 (Dark) +export const orange05 = "#a94d14"; + +// Backgrounds / Orange / Orange-06 +export const orange06 = "#cf5d14"; + +// Backgrounds / Orange / -Orange-07 (Base) +export const orange07 = "#f66e13"; + +// Backgrounds / Orange / Orange-08 +export const orange08 = "#fd853f"; + +// Backgrounds / Orange / Orange-09 (Light) +export const orange09 = "#ff9c62"; + +// Backgrounds / Orange / Orange-10 +export const orange10 = "#ffb284"; + +// Backgrounds / Orange / Orange-11 (Xlight) +export const orange11 = "#ffc8a7"; + +// Backgrounds / Orange / Orange-12 +export const orange12 = "#ffdbc5"; + +// Backgrounds / Orange / Orange-13 +export const orange13 = "#ffeee5"; + +// Backgrounds / Orange / Orange-14 (Xxlight) +export const orange14 = "#fdf9f7"; + +// Transparents / Orange / 00-Orange-Light (70%) +export const orangeLight = "rgba(246,110,19,0.7)"; + +// Transparents / Orange / 01-Orange-Xlight (40%) +export const orangeXLight = "rgba(246,110,19,0.4)"; + +// Transparents / Orange / 02-Orange-Xxlight (10%) +export const orangeXXLight = "rgba(246,110,19,0.1)"; + +// Backgrounds / Pear / Pear-00 (Xxdark) +export const pear00 = "#1e1d00"; + +// Backgrounds / Pear / Pear-01 +export const pear01 = "#2a2a07"; + +// Backgrounds / Pear / Pear-02 +export const pear02 = "#42410e"; + +// Backgrounds / Pear / Pear-03 (Xdark) +export const pear03 = "#5d5a12"; + +// Backgrounds / Pear / Pear-04 +export const pear04 = "#7c7815"; + +// Backgrounds / Pear / Pear-05 (Dark) +export const pear05 = "#9d9718"; + +// Backgrounds / Pear / Pear-06 +export const pear06 = "#bfb71b"; + +// Backgrounds / Pear / -Pear-07 (Base) +export const pear07 = "#e3d91c"; + +// Backgrounds / Pear / Pear-08 +export const pear08 = "#eade4f"; + +// Backgrounds / Pear / Pear-09 (Light) +export const pear09 = "#f0e472"; + +// Backgrounds / Pear / Pear-10 +export const pear10 = "#f6e993"; + +// Backgrounds / Pear / Pear-11 (Xlight) +export const pear11 = "#f9efb2"; + +// Backgrounds / Pear / Pear-12 +export const pear12 = "#fcf4cd"; + +// Backgrounds / Pear / Pear-13 +export const pear13 = "#fdf9e8"; + +// Backgrounds / Pear / Pear-14 (Xxlight) +export const pear14 = "#fdfcf7"; + +// Transparents / Pear / 00-Pear-Light (70%) +export const pearLight = "rgba(227,217,28,0.7)"; + +// Transparents / Pear / 01-Pear-Xlight (40%) +export const pearXLight = "rgba(227,217,28,0.4)"; + +// Transparents / Pear / 02-Pear-Xxlight (10%) +export const pearXXLight = "rgba(227,217,28,0.1)"; + +// Backgrounds / Pink / Pink-00 (Xxdark) +export const pink00 = "#1e000a"; + +// Backgrounds / Pink / Pink-01 +export const pink01 = "#280a14"; + +// Backgrounds / Pink / Pink-02 +export const pink02 = "#3f1221"; + +// Backgrounds / Pink / Pink-03 (Xdark) +export const pink03 = "#58192f"; + +// Backgrounds / Pink / Pink-04 +export const pink04 = "#75223f"; + +// Backgrounds / Pink / Pink-05 (Dark) +export const pink05 = "#942b50"; + +// Backgrounds / Pink / Pink-06 +export const pink06 = "#b53461"; + +// Backgrounds / Pink / -Pink-07 (Base) +export const pink07 = "#d63d73"; + +// Backgrounds / Pink / Pink-08 +export const pink08 = "#e06187"; + +// Backgrounds / Pink / Pink-09 (Light) +export const pink09 = "#e87f9c"; + +// Backgrounds / Pink / Pink-10 +export const pink10 = "#f09cb1"; + +// Backgrounds / Pink / Pink-11 (Xlight) +export const pink11 = "#f5b8c6"; + +// Backgrounds / Pink / Pink-12 +export const pink12 = "#f9d1da"; + +// Backgrounds / Pink / Pink-13 +export const pink13 = "#fce9ee"; + +// Backgrounds / Pink / Pink-14 (Xxlight) +export const pink14 = "#fdf7f9"; + +// Transparents / Pink / 00-Pink-Light (70%) +export const pinkLight = "rgba(214,61,115,0.7)"; + +// Transparents / Pink / 01-Pink-Xlight (40%) +export const pinkXLight = "rgba(214,61,115,0.4)"; + +// Transparents / Pink / 02-Pink-Xxlight (10%) +export const pinkXXLight = "rgba(214,61,115,0.1)"; + +// Backgrounds / Red / Red-00 (Xxdark) +export const red00 = "#1e0002"; + +// Backgrounds / Red / Red-01 +export const red01 = "#2a0805"; + +// Backgrounds / Red / Red-02 +export const red02 = "#420e0b"; + +// Backgrounds / Red / Red-03 (Xdark) +export const red03 = "#5d110f"; + +// Backgrounds / Red / Red-04 +export const red04 = "#7d1311"; + +// Backgrounds / Red / Red-05 (Dark) +export const red05 = "#9e1313"; + +// Backgrounds / Red / Red-06 +export const red06 = "#c11014"; + +// Backgrounds / Red / -Red-07 (Base) +export const red07 = "#e50914"; + +// Backgrounds / Red / Red-08 +export const red08 = "#f04c38"; + +// Backgrounds / Red / Red-09 (Light) +export const red09 = "#f9715a"; + +// Backgrounds / Red / Red-10 +export const red10 = "#ff927d"; + +// Backgrounds / Red / Red-11 (Xlight) +export const red11 = "#ffb2a2"; + +// Backgrounds / Red / Red-12 +export const red12 = "#ffcdc3"; + +// Backgrounds / Red / Red-13 +export const red13 = "#ffe8e4"; + +// Backgrounds / Red / Red-14 (Xxlight) +export const red14 = "#fdf7f8"; + +// Transparents / Red / 00-Red-Light (70%) +export const redLight = "rgba(229,9,20,0.7)"; + +// Transparents / Red / 01-Red-Xlight (40%) +export const redXLight = "rgba(229,9,20,0.4)"; + +// Transparents / Red / 02-Red-Xxlight (10%) +export const redXXLight = "rgba(229,9,20,0.1)"; + +// Backgrounds / Violet / Violet-00 (Xxdark) +export const violet00 = "#08001e"; + +// Backgrounds / Violet / Violet-01 +export const violet01 = "#110b2b"; + +// Backgrounds / Violet / Violet-02 +export const violet02 = "#1d1643"; + +// Backgrounds / Violet / Violet-03 (Xdark) +export const violet03 = "#2a1f5d"; + +// Backgrounds / Violet / Violet-04 +export const violet04 = "#3b297c"; + +// Backgrounds / Violet / Violet-05 (Dark) +export const violet05 = "#4c349d"; + +// Backgrounds / Violet / Violet-06 +export const violet06 = "#5e3fbf"; + +// Backgrounds / Violet / -Violet-07 (Base) +export const violet07 = "#714be2"; + +// Backgrounds / Violet / Violet-08 +export const violet08 = "#8c66e7"; + +// Backgrounds / Violet / Violet-09 (Light) +export const violet09 = "#a481ec"; + +// Backgrounds / Violet / Violet-10 +export const violet10 = "#ba9cf1"; + +// Backgrounds / Violet / Violet-11 (Xlight) +export const violet11 = "#ceb8f5"; + +// Backgrounds / Violet / Violet-12 +export const violet12 = "#dfd0f8"; + +// Backgrounds / Violet / Violet-13 +export const violet13 = "#f0e9fb"; + +// Backgrounds / Violet / Violet-14 (Xxlight) +export const violet14 = "#f9f7fd"; + +// Transparents / Violet / 00-Violet-Light (70%) +export const violetLight = "rgba(113,75,226,0.7)"; + +// Transparents / Violet / 01-Violet-Xlight (40%) +export const violetXLight = "rgba(113,75,226,0.4)"; + +// Transparents / Violet / 02-Violet-Xxlight (10%) +export const violetXXLight = "rgba(113,75,226,0.1)"; + +// Backgrounds / White +export const white = "#FFFFFF"; + +// Transparents / White / 00-White-Light (70%) +export const whiteLight = "rgba(255,255,255,0.7)"; + +// Transparents / White / 01-White-Xlight (40%) +export const whiteXLight = "rgba(255,255,255,0.4)"; + +// Transparents / White / 02-White-Xxlight (10%) +export const whiteXXLight = "rgba(255,255,255,0.1)"; + +// Backgrounds / Yellow / Yellow-00 (Xxdark) +export const yellow00 = "#1e1400"; + +// Backgrounds / Yellow / Yellow-01 +export const yellow01 = "#2c1e06"; + +// Backgrounds / Yellow / Yellow-02 +export const yellow02 = "#47300d"; + +// Backgrounds / Yellow / Yellow-03 (Xdark) +export const yellow03 = "#64430f"; + +// Backgrounds / Yellow / Yellow-04 +export const yellow04 = "#875a11"; + +// Backgrounds / Yellow / Yellow-05 (Dark) +export const yellow05 = "#ac7210"; + +// Backgrounds / Yellow / Yellow-06 +export const yellow06 = "#d38a0c"; + +// Backgrounds / Yellow / -Yellow-07 (Base) +export const yellow07 = "#fba404"; + +// Backgrounds / Yellow / Yellow-08 +export const yellow08 = "#ffb141"; + +// Backgrounds / Yellow / Yellow-09 (Light) +export const yellow09 = "#ffbf66"; + +// Backgrounds / Yellow / Yellow-10 +export const yellow10 = "#ffcd89"; + +// Backgrounds / Yellow / Yellow-11 (Xlight) +export const yellow11 = "#ffdbaa"; + +// Backgrounds / Yellow / Yellow-12 +export const yellow12 = "#ffe7c8"; + +// Backgrounds / Yellow / Yellow-13 +export const yellow13 = "#fff4e6"; + +// Backgrounds / Yellow / Yellow-14 (Xxlight) +export const yellow14 = "#fdfbf7"; + +// Transparents / Yellow / 00-Yellow-Light (70%) +export const yellowLight = "rgba(251,164,4,0.7)"; + +// Transparents / Yellow / 01-Yellow-Xlight (40%) +export const yellowXLight = "rgba(251,164,4,0.4)"; + +// Transparents / Yellow / 02-Yellow-Xxlight (10%) +export const yellowXXLight = "rgba(251,164,4,0.1)"; + +// Primary Green color +export const primaryGreen = "#40BA56"; + +// TagChip Colors + +//success +export const successTag = "#9FDCAA"; + +//progress +export const progressTag = "#8DE0F9"; + +//error +export const errorTag = "#FBB4C6"; + +//warning +export const warningTag = "#FCD181"; + +//warning +export const otherTag = "#C8ABFF"; + +// Wait task type button +export const blackish = "#1f1f1f"; +export const gray15 = "#a5a5a5"; + +// User roles tag colors + +//admin +export const roleAdmin = "#9FDCAA"; + +//readonly +export const roleReadOnly = "#DDDDDD"; + +//user +export const roleUser = "#C8ABFF"; + +//workflow-manager +export const roleWfManager = "#8DE0F9"; + +//workflow-manager +export const roleMetaManager = "#FCD181"; + +// New Sidebar colors +export const sidebarBlacky = "#060606"; +export const sidebarGreyText = "#858585"; +export const sidebarGreyDark = "#161616"; +export const sidebarFaintGrey = "#DDDDDD"; +export const sidebarGrey = "#494949"; +export const sidebarBarelyPastWhite = "#F3F3F3"; +export const sidebarVersion = "#9C9C9C"; +export const sidebarUIVersion = "#494949"; + +// purple color in new theme + +export const purple = "#9157FF"; +export const lightPurple = "#c8abff"; + +export const greyBorder = "#DDDDDD"; +export const greyText = "#858585"; +export const greyText2 = "#AFAFAF"; + +// colors for SearchEverything and UIModal +export const sePurple = purple; +export const seGrey = gray14; +export const seGrey2 = "#AFAFAF"; +// default modal backdropColor +export const defaultModalBackdropColor = "#AFAFAF"; + +// colors for tabs v1 +export const tabsColor = "#494949"; +export const tabActiveColor = "#060606"; +export const tabBackground = "#DDDDDD"; +export const tabsContainerBg = "#F3F3F3"; + +export const modalBackdropColor = "#090a11cc"; +export const colorfullGradient = + "linear-gradient(0.40turn,rgba(28, 177, 255,0.4), rgba(25, 118, 210,0.4), rgba(145, 87, 255,0.4), rgba(255, 106, 128,0.4));"; +export const blueLight = "#0D94DB"; +export const blueLightMode = "#1976D2"; +export const darkBlueLightMode = "#1976D2"; +export const lightBlueHoverBg = "#CAE0F5"; +export const blueBackground = "#1CB1FF"; +export const greyBg = "#252525"; +export const primaryHoverBoxShadow = "#04386C"; +export const secondaryHoverBoxShadow = "#095096"; +export const tertiaryHoverBoxShadow = "#4D4D4D"; +export const orkesBrandN200 = "#D7DCDD"; +export const orkesBrandS600 = "#189ED3"; +export const secondaryBlack = "#060606"; + +export const errorRed = "#D6423B"; + +// disabled input +export const lightGrey = "#ECECEC"; +export const greenBackground = "#2e7d32"; + +export const lightGreyBackground = "#EFF0F0"; +// cyan +export const cyan = "#00FFFF"; + +export const trialExpiredBannerBg = "#FCF0EE"; +export const trailExpiredTextColor = "#D84A44"; diff --git a/ui-next/src/theme/tokens/globalConstants.js b/ui-next/src/theme/tokens/globalConstants.js new file mode 100644 index 0000000000..58ec5aabf7 --- /dev/null +++ b/ui-next/src/theme/tokens/globalConstants.js @@ -0,0 +1 @@ +export const INNER_HEADER_LEVEL = 2; diff --git a/ui-next/src/theme/tokens/orkes-theme.js b/ui-next/src/theme/tokens/orkes-theme.js new file mode 100644 index 0000000000..a7f32665bb --- /dev/null +++ b/ui-next/src/theme/tokens/orkes-theme.js @@ -0,0 +1,37 @@ +import Color from "color"; + +const orkesThemeBase = { + background: "#AAAAAA", + primary: "#1976d2", + // TODO: Define these + // text: "#333333?", + // danger: "#??????", + // success: "#??????", + // warning: "#??????", +}; + +const generateColorShades = (name, color) => { + return { + [`${name}Darkest`]: Color(color).darken(0.6).hex(), + [`${name}Darker`]: Color(color).darken(0.4).hex(), + [`${name}Dark`]: Color(color).darken(0.2).hex(), + [`${name}Light`]: Color(color).lighten(0.2).hex(), + [`${name}Lighter`]: Color(color).lighten(0.4).hex(), + [`${name}Lightest`]: Color(color).lighten(0.6).hex(), + }; +}; + +const orkesThemeWithShades = Object.entries(orkesThemeBase).reduce( + (acc, [name, color]) => { + return { + ...acc, + ...generateColorShades(name, color), + }; + }, + {}, +); + +export const orkesTheme = { + ...orkesThemeBase, + ...orkesThemeWithShades, +}; diff --git a/ui-next/src/theme/tokens/variables.ts b/ui-next/src/theme/tokens/variables.ts new file mode 100644 index 0000000000..2366276edb --- /dev/null +++ b/ui-next/src/theme/tokens/variables.ts @@ -0,0 +1,73 @@ +import { colorOverrides } from "./colorOverrides"; + +// We dont seem to be using this.. + +//import { orkesTheme } from "./orkes-theme"; + +export const colors = { + ...colorOverrides, + background: "#AAAAAA", + primary: "#1976d2", +}; + +export const fontSizes = { + fontSize0: "10px", + fontSize1: "12px", + fontSize2: "13px", + fontSize3: "14px", + fontSize4: "16px", + fontSize5: "18px", + fontSize6: "20px", + fontSize7: "24px", + fontSize8: "28px", + fontSize9: "32px", + fontSize10: "40px", + fontSize11: "52px", + fontSize12: "68px", + fontSize13: "88px", +}; +export const lineHeights = { + lineHeight0: 1.25, + lineHeight1: 1.5, +}; + +export const fontWeights = { + fontWeight0: 300, + fontWeight1: 400, + fontWeight2: 500, + fontWeight3: 600, +}; + +export const fontFamily = { + fontFamilySans: + '"Lexend", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"', + fontFamilyMono: "monospace", +}; + +export const spacings = { + space0: "4px", + space1: "8px", + space2: "12px", + space3: "16px", + space4: "20px", + space5: "24px", + space6: "32px", + space7: "48px", + space8: "80px", + space9: "144px", +}; + +export const breakpoints = { + xsmall: "599px", + small: "1023px", + medium: "1439px", + large: "1919px", + xlarge: "3840px", +}; + +export const borders = { + radiusSmall: "4px", + blueRegular2px: "2px solid rgba(31,131,219,1)", + blackRegular1px: "1px solid rgba(5,5,5,1)", + blackLight1px: "1px solid rgba(5,5,5,0.7)", +}; diff --git a/ui-next/src/types/Application.ts b/ui-next/src/types/Application.ts new file mode 100644 index 0000000000..cdfe4d49e3 --- /dev/null +++ b/ui-next/src/types/Application.ts @@ -0,0 +1,10 @@ +import { Tag } from "./common"; +export interface Application { + id: string; + name: string; + createdBy: string; + updatedBy: string; + createTime: number; + updateTime: number; + tags: Tag[]; +} diff --git a/ui-next/src/types/CloudTemplateResults.ts b/ui-next/src/types/CloudTemplateResults.ts new file mode 100644 index 0000000000..ef31ac93f1 --- /dev/null +++ b/ui-next/src/types/CloudTemplateResults.ts @@ -0,0 +1,54 @@ +import { CommonTaskDef, WorkflowDef } from "types"; +import { HumanTemplate } from "types/HumanTaskTypes"; +import { IntegrationI, ModelDto } from "types/Integrations"; +import { PromptDef } from "types/Prompts"; +import { SchemaDefinition } from "types/SchemaDefinition"; + +export type WorkflowResult = { + success: boolean; + message?: string; + workflow: WorkflowDef; +}; + +export type TaskResult = { + success: boolean; + message?: string; + task: CommonTaskDef; +}; + +export type SchemaResult = { + success: boolean; + message?: string; + schema: SchemaDefinition; +}; + +export type IntegrationResult = { + success: boolean; + message?: string; + integration: IntegrationI; +}; + +export type ModelResult = { + success: boolean; + message?: string; + model: ModelDto; +}; + +export type PromptResult = { + success: boolean; + message?: string; + prompt: PromptDef; +}; + +export type IntegrationAndModelResult = { + success: boolean; + message?: string; + integration: IntegrationI; + modelResults: ModelResult[]; +}; + +export type HumanTemplateResult = { + success: boolean; + message?: string; + userForm: HumanTemplate; +}; diff --git a/ui-next/src/types/CloudTemplateType.ts b/ui-next/src/types/CloudTemplateType.ts new file mode 100644 index 0000000000..72db79228f --- /dev/null +++ b/ui-next/src/types/CloudTemplateType.ts @@ -0,0 +1,81 @@ +import { IntegrationI, ModelDto } from "./Integrations"; +import { PromptDef } from "./Prompts"; + +// The original type is IntegrationDef, is broken, it is used for configuring an integration and as integration pulled from the server which is not the same + +export type IntegrationAndModel = { + integration: IntegrationI; + models: ModelDto[]; +}; + +export type CloudTemplateTypeV1 = { + id: string; + title: string; + description: string; + featureLabelAndLinks: { + [key: string]: string; + }; + githubProjectLinks: { + [key: string]: string; + }; + workflowDefinitionGithubLink: string; + workflowTemplateDefLink?: string; + taskDefinitionsGithubLink?: string; + taskTemplateDefsLink?: string; + userFormsGithubLink?: string; + userFormTemplateDefLink?: string; + schemasGithubLink?: string; + schemaDefTemplateLink?: string; + helpDocumentationLink?: string; + integrationAndModelsGithubLink?: string; + promptsGithubLink?: string; + thumbnailUrl?: string; + category: string; + tags?: string[]; + version: 1; + createdAt: string; + createdBy: string; + updatedAt: string; + updatedBy: string; +}; + +export type PlaceholderDef = { + key: string; + value: string; +}; + +export type CloudTemplateTypeV2 = { + id: string; + title: string; + description: string; + featureLabelAndLinks: { + [key: string]: string; + }; + githubProjectLinks: { + [key: string]: string; + }; + + taskDefinitions: Record[]; + workflowDefinitions: Record[]; + helpDocumentationLink?: string; + userForms: Record[]; + schemas: Record[]; + integrationsWithModels: IntegrationAndModel[]; + secrets: Record[]; + environmentVariables: Record[]; + schedules: Record[]; + webhooks: Record[]; + remoteServices: Record[]; + prompts: PromptDef[]; + placeholders: PlaceholderDef[]; + thumbnailUrl?: string; + category: string; + tags?: string[]; + version: 2; + createdAt: string; + createdBy: string; + updatedAt: string; + updatedBy: string; +}; + +export type CloudTemplateType = CloudTemplateTypeV1 | CloudTemplateTypeV2; diff --git a/ui-next/src/types/Crumbs.ts b/ui-next/src/types/Crumbs.ts new file mode 100644 index 0000000000..9507e5e35f --- /dev/null +++ b/ui-next/src/types/Crumbs.ts @@ -0,0 +1,12 @@ +import { TaskDef, TaskType } from "./common"; + +export type Crumb = { + refIdx: number; + forkIndex?: number; + ref: string; + parent?: string | null; + decisionBranch?: string; + type: TaskType; +}; + +export type CrumbMap = Record; //Using record for now since xstate inspector wont show Map. diff --git a/ui-next/src/types/EnvVariables.ts b/ui-next/src/types/EnvVariables.ts new file mode 100644 index 0000000000..b24190f74a --- /dev/null +++ b/ui-next/src/types/EnvVariables.ts @@ -0,0 +1,7 @@ +import { TagDto } from "./Tag"; + +export interface EnvironmentVariables { + name: string; + value: string; + tags: TagDto[]; +} diff --git a/ui-next/src/types/Environment.ts b/ui-next/src/types/Environment.ts new file mode 100644 index 0000000000..eb3b28dc7f --- /dev/null +++ b/ui-next/src/types/Environment.ts @@ -0,0 +1,5 @@ +export interface EnvironmentDTO { + id: string; + tokenSecret: string | null; + uri: string; +} diff --git a/ui-next/src/types/Events.ts b/ui-next/src/types/Events.ts new file mode 100644 index 0000000000..a1766669ae --- /dev/null +++ b/ui-next/src/types/Events.ts @@ -0,0 +1,76 @@ +import { TagDto } from "./Tag"; + +export type CompleteActionType = { + action: "complete_task"; + expandInlineJSON: boolean; + complete_task: { + workflowId?: string; + taskRefName?: string; + taskId?: string; + output?: Record; + }; +}; + +export type FailActionType = { + action: "fail_task"; + expandInlineJSON: boolean; + fail_task: { + workflowId?: string; + taskRefName?: string; + taskId?: string; + output?: Record; + }; +}; + +export type UpdateWorkFlowVariableType = { + action: "update_workflow_variables"; + expandInlineJSON: boolean; + update_workflow_variables: { + workflowId: string; + appendArray?: boolean; + variables?: Record; + }; +}; + +export type StartWorkflowAction = { + action: "start_workflow"; + start_workflow: { + name: string; + version: string; + correlationId: string; + idempotencyKey?: string; + idempotencyStrategy?: string; + input?: Record; + taskToDomain?: { + [key: string]: string; + }; + }; + expandInlineJSON: boolean; +}; + +export type TerminateWorkflowAction = { + action: "terminate_workflow"; + expandInlineJSON: boolean; + terminate_workflow: { + workflowId: string; + terminationReason: string; + }; +}; + +export type ConductorEvent = { + name: string; + description?: string; + event: string; + evaluatorType: string; + condition: string; + actions: Array< + | CompleteActionType + | FailActionType + | UpdateWorkFlowVariableType + | StartWorkflowAction + | TerminateWorkflowAction + >; + active: boolean; + ownerEmail: string; + tags?: TagDto[]; +}; diff --git a/ui-next/src/types/Execution.ts b/ui-next/src/types/Execution.ts new file mode 100644 index 0000000000..8fa7d7e763 --- /dev/null +++ b/ui-next/src/types/Execution.ts @@ -0,0 +1,131 @@ +import { TaskStatus } from "./TaskStatus"; +import { TaskType, TaskDef } from "./common"; +import { WorkflowDef } from "./WorkflowDef"; + +export type ExecutedData = { + status: TaskStatus; + executed: boolean; + attempts: number; + collapsed?: boolean; + collapsedTasks?: TaskDef[]; + parentTaskReferenceName?: string; + collapsedTasksStatus?: string[]; + outputData?: Record; + parentLoop?: TaskDef; +}; + +type ForkedExecutionTaskInputData = { + forkedTasks: string[]; + forkedTaskDefs: TaskDef[]; + docLink?: string; +}; + +export interface ExecutionTask< + T = ForkedExecutionTaskInputData, +> extends TaskDef { + taskId?: string; + referenceTaskName: string; + taskType: TaskType | string; + workflowTask: { + name: string; + taskReferenceName: string; + type: string; + description?: string; + }; + inputData?: T & { + subWorkflowName?: string; + integrationName?: string; + [key: string]: unknown; + }; + outputData?: { + subWorkflowId?: string; + caseOutput?: string[]; + [key: string]: unknown; + }; + status: TaskStatus; + executed: boolean; + domain?: string; + seq?: string; + scheduledTime?: number; + startTime?: number; + endTime?: number; + updateTime?: number; + callbackAfterSeconds?: number; + pollCount?: number; + workflowType: string; + loopOverTask: boolean; + retryCount?: number; + reasonForIncompletion?: string; + workerId?: string; + correlationId?: string; + queueWaitTime?: number; +} + +// @deprecated use WorkflowExecution instead +export type Execution = { + tasks: ExecutionTask[]; + workflowDefinition: WorkflowDef; +}; + +export enum WorkflowExecutionStatus { + RUNNING = "RUNNING", + COMPLETED = "COMPLETED", + FAILED = "FAILED", + TIMED_OUT = "TIMED_OUT", + TERMINATED = "TERMINATED", + PAUSED = "PAUSED", +} + +export interface WorkflowExecution { + tasks: ExecutionTask[]; + workflowDefinition: WorkflowDef; + correlationId: string; + createdBy: string; + endTime: string; + executionTime: number; + failedReferenceTaskNames: string; + input: string; + inputSize: number; + output: Record; + outputSize: number; + priority: number; + reasonForIncompletion: string; + startTime: string; + status: WorkflowExecutionStatus; + updateTime: string; + version: number; + workflowId: string; + workflowType: string; + variables?: Record; + workflowIntrospection?: WorkflowIntrospectionRecord[]; +} + +export interface DetailedTime { + seconds: number; + nanos: number; +} + +export interface WorkflowIntrospectionRecord { + workflowId: string; + id: string; + parentRecordId?: string; + threadName: string; + taskId?: string; + name: string; + description?: string; + stacktrace: string; + start: DetailedTime; + duration: DetailedTime; + overhead: DetailedTime; + attributes?: Record; +} + +export interface WorkflowExecutionSearch { + queryId: string; + results: WorkflowExecution[]; +} + +export type DoWhileSelection = { + doWhileTaskReferenceName: string; + selectedIteration: number; +}; diff --git a/ui-next/src/types/FormFieldTypes.ts b/ui-next/src/types/FormFieldTypes.ts new file mode 100644 index 0000000000..7c7e32f7cd --- /dev/null +++ b/ui-next/src/types/FormFieldTypes.ts @@ -0,0 +1,28 @@ +export enum UiIntegrationsFieldType { + LLM_PROVIDER = "llmProvider", + MODEL = "model", + PROMPT_NAME = "promptName", + TEXT = "text", + VECTOR_DB = "vectorDB", + NAMESPACE = "namespace", + QUERY = "query", + EMBEDDING_MODEL_PROVIDER = "embeddingModelProvider", + EMBEDDING_MODEL = "embeddingModel", + URL = "url", + MEDIA_TYPE = "mediaType", + DOC_ID = "docId", + TEMPERATURE = "temperature", + TOP_P = "topP", + STOP_WORDS = "stopWords", + MAX_TOKENS = "maxTokens", + INDEX = "index", + EMBEDDINGS = "embeddings", + CHUNK_SIZE = "chunkSize", + CHUNK_OVERLAP = "chunkOverlap", + ID = "id", + INSTRUCTIONS = "instructions", + MESSAGES = "messages", + MAX_RESULTS = "maxResults", + JSON_OUTPUT = "jsonOutput", + DIMENSIONS = "dimensions", +} diff --git a/ui-next/src/types/HumanTaskTypes.ts b/ui-next/src/types/HumanTaskTypes.ts new file mode 100644 index 0000000000..88244621b4 --- /dev/null +++ b/ui-next/src/types/HumanTaskTypes.ts @@ -0,0 +1,158 @@ +import { JsonSchema, Layout } from "@jsonforms/core"; +import { CommonTaskDef } from "./TaskType"; +import { TagDto } from "./Tag"; + +export interface FormRenderProperties { + jsonSchema?: JsonSchema; + templateUI?: Layout; +} + +export interface TemplateDataType extends FormRenderProperties { + createTime?: number; + updateTime?: number; + createdBy?: string; + updatedBy?: string; + name?: string; + version?: number; +} + +export interface HumanTemplate extends FormRenderProperties { + name: string; + version: number; + createdBy?: string; + createTime?: number; + updatedBy?: string; + updateTime?: number; + tags?: TagDto[]; +} + +export enum AssigneeType { + CONDUCTOR_USER = "CONDUCTOR_USER", + CONDUCTOR_GROUP = "CONDUCTOR_GROUP", + EXTERNAL_USER = "EXTERNAL_USER", + EXTERNAL_GROUP = "EXTERNAL_GROUP", +} + +export enum TriggerType { + PENDING = "PENDING", + ASSIGNED = "ASSIGNED", + IN_PROGRESS = "IN_PROGRESS", + COMPLETED = "COMPLETED", + TIMED_OUT = "TIMED_OUT", + ASSIGNEE_CHANGED = "ASSIGNEE_CHANGED", + CLAIMANT_CHANGED = "CLAIMANT_CHANGED", +} + +export type TriggerPolicy = { + startWorkflowRequest: { + correlationId: string; + input: Record; + name: string; + taskToDomain: Record; + version: number; + }; + triggerType?: TriggerType; +}; + +export enum AssignmentCompletionStrategy { + LEAVE_OPEN = "LEAVE_OPEN", + TERMINATE = "TERMINATE", +} + +export type HumanTaskAssignee = { + userType: AssigneeType; + user: string; +}; + +export type HumanTaskAssignment = { + assignee: HumanTaskAssignee; + slaMinutes?: number; +}; + +export type HumanTaskDefinition = { + assignmentCompletionStrategy?: AssignmentCompletionStrategy; + assignments?: HumanTaskAssignment[]; + userFormTemplate?: Partial; + taskTriggers?: Array; + displayName?: string; + autoClaim?: boolean; +}; + +export type HumanTaskInputParams = { + __humanTaskDefinition: HumanTaskDefinition; +}; + +export interface HumanTaskDef extends CommonTaskDef { + inputParameters: HumanTaskInputParams; +} + +export enum HumanTaskState { + IN_PROGRESS = "IN_PROGRESS", + PENDING = "PENDING", + ASSIGNED = "ASSIGNED", + COMPLETED = "COMPLETED", + TIMED_OUT = "TIMED_OUT", + DELETED = "DELETED", +} + +export type ExecutionHumanTaskDefI = { + assignments: HumanTaskAssignment[]; + userFormTemplate: { + name: string; + version: number; + }; + fullTemplate?: TemplateDataType; +}; + +type HumanTaskProcessContext = { + assigneeIndex: number; + lastUpdated: number; + state: HumanTaskState; +}; + +export type HumanExecutionTask = { + assignee: HumanTaskAssignee; + claimant: HumanTaskAssignee; + createdBy: string; + createdOn: number; + definitionName: string; + humanTaskDef: ExecutionHumanTaskDefI; + input: HumanTaskInputParams & + HumanTaskProcessContext & + Record; + taskId: string; + state: HumanTaskState; + workflowId: string; + workflowName: string; + taskRefName: string; + output: Record; +}; + +export enum SearchType { + INBOX = "INBOX", + ADMIN = "ADMIN", +} +export interface HumanTaskSearchQuery { + assignees?: HumanTaskAssignee[]; + claimants?: HumanTaskAssignee[]; + definitionNames?: string[]; + taskOutputQuery?: string; + taskInputQuery?: string; + fullTextQuery?: string; + states?: TriggerType[]; + taskRefNames?: string[]; + workflowNames?: string[]; + query?: string; + size: number; + page: number; + rowsPerPage: number; + updateStartTime?: string; + updateEndTime?: string; + searchType?: SearchType; + searchId?: number; +} + +export type HumanTaskSearchResponse = { + totalHits: number; + results: HumanExecutionTask[]; +}; diff --git a/ui-next/src/types/Integrations.ts b/ui-next/src/types/Integrations.ts new file mode 100644 index 0000000000..fadd71bc83 --- /dev/null +++ b/ui-next/src/types/Integrations.ts @@ -0,0 +1,110 @@ +import { TagDto } from "./Tag"; + +export enum IntegrationType { + AMAZON_MSK = "kafka_msk", + AMQP = "amqp", + ANTHROPIC = "anthropic", + APACHE_KAFKA = "kafka", + AWS = "aws", + AWS_BEDROCK_ANTHROPIC = "aws_bedrock_anthropic", + AWS_BEDROCK_COHERE = "aws_bedrock_cohere", + AWS_BEDROCK_LLAMA2 = "aws_bedrock_llama2", + AWS_BEDROCK_TITAN = "aws_bedrock_titan", + AWS_SQS = "aws_sqs", + AZURE_OPENAI = "azure_openai", + AZURE_SERVICE_BUS = "azure_service_bus", + COHERE = "cohere", + CONFLUENT_KAFKA = "kafka_confluent", + GCP = "gcp", + GCP_PUBSUB = "gcp_pubsub", + GIT = "git", + HUGGING_FACE = "huggingface", + IBM_MQ = "ibm_mq", + MISTRAL = "mistral", + MONGO_VECTOR_DB = "mongovectordb", + NATS_MESSAGING = "nats", + OPENAI = "openai", + PINECONE_DB = "pineconedb", + POSTGRES_VECTOR_DB = "pgvectordb", + RELATIONAL_DB = "relational_db", + VERTEX_AI = "vertex_ai", + VERTEX_AI_GEMINI = "vertex_ai_gemini", + WEAVIATE_DB = "weaviatedb", + PERPLEXITY = "perplexity", + GROK = "Grok", + SENDGRID = "sendgrid", +} +export enum IntegrationCategory { + API = "API", + AI_MODEL = "AI_MODEL", + VECTOR_DB = "VECTOR_DB", + RELATIONAL_DB = "RELATIONAL_DB", + MESSAGE_BROKER = "MESSAGE_BROKER", + EMAIL = "EMAIL", + EVENT_SOURCE = "EVENT_SOURCE", +} + +export type BaseIntegration = { + category: string; + description: string; + enabled: boolean; + name: string; + type: IntegrationType; +}; + +export type IntegrationDef = BaseIntegration & { + // Response from the def endpoint + tags: string[]; + configuration: IntegrationConfigFieldModel[]; + categoryLabel: string; + nameLabel: string; + iconName: string; +}; + +export type IntegrationI = BaseIntegration & { + tags?: TagDto[]; + configuration: Record; +}; + +export type ModelDto = { + createTime?: number; + updateTime?: number; + createdBy?: string; + updatedBy?: string; + integrationName: string; + api: string; + description: string; + configuration: { + [key: string]: string; + }; + enabled: boolean; + tags: TagDto[]; + endpoint?: string; +}; + +export type IntegrationConfigFieldModel = { + description: string; + fieldName: string; + fieldType: string; + label: string; + valueOptions?: { + label: string; + value: string; + }[]; + optional: boolean; + dependsOn?: { + fieldName: string; + value: string; + }[]; + value?: string; +}; + +export type IntegrationConfigFormField = { + name: string; + label: string; + helperText: string; + type: string; + value: string | boolean; + optional: boolean; + options?: { label: string; value: string }[]; +}; diff --git a/ui-next/src/types/Messages.ts b/ui-next/src/types/Messages.ts new file mode 100644 index 0000000000..2c89963247 --- /dev/null +++ b/ui-next/src/types/Messages.ts @@ -0,0 +1,6 @@ +import { AlertColor } from "@mui/material"; + +export interface PopoverMessage { + text: string; + severity: AlertColor; +} diff --git a/ui-next/src/types/MetricsTypes.ts b/ui-next/src/types/MetricsTypes.ts new file mode 100644 index 0000000000..6aafbb42f4 --- /dev/null +++ b/ui-next/src/types/MetricsTypes.ts @@ -0,0 +1,20 @@ +export interface HistoricalData { + p50: number; + p75: number; + p90: number; + p95: number; + p99: number; + errorCount: number; + requestCount: number; + cacheHits: number; + cacheMisses: number; + time: number; + errorsByStatusCode?: Record; +} + +export interface FormattedHistoricalData extends Omit { + time: Date | null; + requests: number; + errors: number; + errorsByStatusCode: Record; +} diff --git a/ui-next/src/types/Prompts.ts b/ui-next/src/types/Prompts.ts new file mode 100644 index 0000000000..99ccbbe10f --- /dev/null +++ b/ui-next/src/types/Prompts.ts @@ -0,0 +1,12 @@ +export type PromptDef = { + name: string; + createdBy?: string; + createTime?: number; + template: string; + updatedBy?: string; + updateTime?: string; + description?: string; + variables: string[]; + integrations: string[]; + version?: number; +}; diff --git a/ui-next/src/types/RemoteServiceTypes.ts b/ui-next/src/types/RemoteServiceTypes.ts new file mode 100644 index 0000000000..75c76d44b0 --- /dev/null +++ b/ui-next/src/types/RemoteServiceTypes.ts @@ -0,0 +1,88 @@ +/** + * Shared types for Remote Service definitions. + * These are here (instead of pages/remoteServices) so that components in + * src/components/ can reference them without importing from an enterprise page. + */ + +export enum ServiceType { + HTTP = "HTTP", + GRPC = "gRPC", + MCP_REMOTE = "MCP_REMOTE", +} + +export interface Method { + id?: number; + operationName: string; + methodName: string; + methodType?: + | "GET" + | "POST" + | "PUT" + | "DELETE" + | "PATCH" + | "UNARY" + | "SERVER_STREAMING" + | "CLIENT_STREAMING" + | "BIDIRECTIONAL_STREAMING"; + inputType: string; + outputType: string; + exampleInput?: Record; + requestContentType?: string; + responseContentType?: string; + description?: string; + deprecated?: boolean; + requestParams?: { + name: string; + type: string; + required: boolean; + schema?: { type?: string }; + }[]; +} + +type CircuitBreakerConfig = { + failureRateThreshold?: number; + slidingWindowSize?: number; + minimumNumberOfCalls?: number; + waitDurationInOpenState?: number; + permittedNumberOfCallsInHalfOpenState?: number; + slowCallRateThreshold?: number; + slowCallDurationThreshold?: number; + automaticTransitionFromOpenToHalfOpenEnabled?: boolean; + maxWaitDurationInHalfOpenState?: number; +}; + +type Config = { + circuitBreakerConfig: CircuitBreakerConfig; +}; + +export type Server = { + url: string; + type: "OPENAPI_SPEC" | "USER_DEFINED"; +}; + +type commonServiceDef = { + name: string; + type: string; + methods: Method[]; + serviceURI?: string; + config: Config; + circuitBreakerEnabled?: boolean; + servers?: Server[]; + authMetadata?: { + key: string; + value: string; + }; +}; + +export interface HttpServiceDefDto extends commonServiceDef { + swaggerUrl: string; + bearerToken?: string; + selectedServerUrl?: string; +} + +export interface GrpcServiceDefDto extends commonServiceDef { + host: string; + port: number; +} + +export type ServiceDefDto = HttpServiceDefDto & GrpcServiceDefDto; diff --git a/ui-next/src/types/Schedulers.ts b/ui-next/src/types/Schedulers.ts new file mode 100644 index 0000000000..2dbc99db34 --- /dev/null +++ b/ui-next/src/types/Schedulers.ts @@ -0,0 +1,32 @@ +import { IObject } from "types/common"; +import { TagDto } from "./Tag"; + +export interface IStartWorkflowRequest { + name: string; + version: number; + input?: IObject; + taskToDomain?: IObject; + priority?: number; +} + +export interface IScheduleDto { + name: string; + cronExpression: string; + runCatchupScheduleInstances?: boolean; + paused?: boolean; + pausedReason?: string; + active?: boolean; + startWorkflowRequest?: IStartWorkflowRequest; + createTime?: number; + updatedTime?: number; + createdBy?: string; + updatedBy?: string; + lastRunTimeInEpoch?: number; + nextRunTime?: number; + tags?: TagDto[]; +} + +export interface SchedulerSearchResult { + results: IScheduleDto[]; + totalHits: number; +} diff --git a/ui-next/src/types/SchemaDefinition.ts b/ui-next/src/types/SchemaDefinition.ts new file mode 100644 index 0000000000..148ee8fe20 --- /dev/null +++ b/ui-next/src/types/SchemaDefinition.ts @@ -0,0 +1,14 @@ +import { JsonSchema } from "@jsonforms/core"; +import { Tag } from "./common"; + +export type SchemaDefinition = { + name: string; + version: number; + data: JsonSchema; + type: "JSON"; + createdBy: string; + updatedBy: string; + createTime: number; + updateTime: number; + tags: Tag[]; +}; diff --git a/ui-next/src/types/Schemas.ts b/ui-next/src/types/Schemas.ts new file mode 100644 index 0000000000..e1605cc5db --- /dev/null +++ b/ui-next/src/types/Schemas.ts @@ -0,0 +1,1834 @@ +import { UpdateTaskStatus } from "./UpdateTaskStatus"; +import { + GetSignedJWTAlgorithmType, + HTTPMethods, + JDBCType, + QueryProcessorType, +} from "./TaskType"; +import { TaskType } from "./common"; +import { TimeoutPolicy } from "types/TimeoutPolicy"; +import { + TASK_NAME_REGEX, + WORKFLOW_NAME_REGEX, + regexToString, +} from "utils/constants/regex"; +import { WORKFLOW_NAME_ERROR_MESSAGE } from "utils/constants/common"; + +const variablePattern = ".*\\$\\{.*"; + +export const nameSchema = { + $id: "/properties/tasks/properties/name", + type: "string", + pattern: regexToString(TASK_NAME_REGEX), + title: "Task name", + description: "Task name", + default: "", +}; + +export const taskReferenceName = { + $id: "/properties/tasks/properties/taskReferenceName", + type: "string", + title: "Task Reference Name", + minLength: 2, + description: + "A unique task reference name for this task in the entire workflow", +}; + +export const inputParameters = { + $id: "/properties/tasks/properties/inputParameters", + anyOf: [ + { + type: "object", + properties: {}, + additionalProperties: true, + }, + { + type: "string", + pattern: variablePattern, + }, + ], + title: "Input Parameters", + description: "Task input parameters", + default: {}, +}; + +export const genericSchema = { + $id: "/properties/tasks/generic", + type: "object", + description: "Generic Schema", + default: { + name: "generic_schema", + taskReferenceName: "generic_schema_ref", + type: TaskType.USER_DEFINED, + }, + required: ["type"], + properties: { + name: { $ref: nameSchema.$id }, + taskReferenceName: { $ref: taskReferenceName.$id }, + type: { + type: "string", + enum: [ + TaskType.DECISION, + TaskType.START, + TaskType.USER_DEFINED, + TaskType.LAMBDA, + TaskType.TERMINAL, + TaskType.EXCLUSIVE_JOIN, + TaskType.WAIT_FOR_EVENT, + TaskType.IA_TASK, + TaskType.LLM_TEXT_COMPLETE, + TaskType.LLM_GENERATE_EMBEDDINGS, + TaskType.LLM_GET_EMBEDDINGS, + TaskType.LLM_STORE_EMBEDDINGS, + TaskType.LLM_SEARCH_INDEX, + TaskType.LLM_INDEX_DOCUMENT, + TaskType.GET_DOCUMENT, + TaskType.LLM_INDEX_TEXT, + TaskType.JUMP, + TaskType.LLM_CHAT_COMPLETE, + TaskType.GRPC, + TaskType.MCP, + ], + }, + }, + additionalProperties: true, +}; + +export const simpleTaskSchema = { + $id: "/properties/tasks/simple", + type: "object", + description: "Simple task", + default: { + name: "simple_task", + taskReferenceName: "simple_task_ref", + inputParameters: {}, + type: TaskType.SIMPLE, + }, + required: ["name", "taskReferenceName", "type"], + properties: { + name: { $ref: nameSchema.$id }, + taskReferenceName: { $ref: taskReferenceName.$id }, + inputParameters: { + $ref: inputParameters.$id, + }, + type: { const: TaskType.SIMPLE }, + }, + additionalProperties: true, +}; + +export const yieldTaskSchema = { + $id: "/properties/tasks/yield", + type: "object", + description: "Yield task", + default: { + name: "yield_task", + taskReferenceName: "yield_task_ref", + inputParameters: {}, + type: TaskType.YIELD, + }, + required: ["name", "taskReferenceName", "type"], + properties: { + name: { $ref: nameSchema.$id }, + taskReferenceName: { $ref: taskReferenceName.$id }, + inputParameters: { + $ref: inputParameters.$id, + }, + type: { const: TaskType.YIELD }, + }, + additionalProperties: true, +}; + +export const doWhileSchema = { + $id: "/properties/tasks/doWhile", + type: "object", + description: "Do While", + default: { + name: "do_while_ref", + taskReferenceName: "do_while_ref", + inputParameters: {}, + type: TaskType.DO_WHILE, + }, + required: [ + "name", + "taskReferenceName", + "inputParameters", + "type", + "loopOver", + "loopCondition", + ], + properties: { + name: { $ref: nameSchema.$id }, + taskReferenceName: { $ref: taskReferenceName.$id }, + inputParameters: { + $ref: inputParameters.$id, + }, + loopCondition: { + type: "string", + }, + loopOver: { + $ref: "/properties/tasks", + }, + type: { const: TaskType.DO_WHILE }, + }, + additionalProperties: true, +}; + +export const eventTaskSchema = { + $id: "/properties/tasks/eventTaskSchema", + type: "object", + description: "Join task", + default: { + name: "join_task_ref", + taskReferenceName: "join_task_ref", + inputParameters: {}, + type: TaskType.EVENT, + }, + required: ["name", "taskReferenceName", "sink", "type"], + properties: { + name: { $ref: nameSchema.$id }, + taskReferenceName: { $ref: taskReferenceName.$id }, + sink: { + type: "string", + }, + inputParameters: { + $ref: inputParameters.$id, + }, + type: { const: TaskType.EVENT }, + }, + additionalProperties: true, +}; + +export const joinTaskSchema = { + $id: "/properties/tasks/joinTaskSchema", + type: "object", + description: "Join task", + default: { + name: "join_task_ref", + taskReferenceName: "join_task_ref", + inputParameters: {}, + type: TaskType.JOIN, + }, + required: ["name", "taskReferenceName", "joinOn", "type"], + properties: { + name: { $ref: nameSchema.$id }, + taskReferenceName: { $ref: taskReferenceName.$id }, + joinOn: { + type: "array", + items: { + type: "string", + }, + }, + inputParameters: { + $ref: inputParameters.$id, + }, + evaluatorType: { + type: "string", + }, + expression: { + type: "string", + }, + type: { const: TaskType.JOIN }, + }, + additionalProperties: true, +}; + +export const forkTaskSchema = { + $id: "/properties/tasks/forkJoin", + type: "object", + description: "Fork task", + default: { + name: "fork_task_ref", + taskReferenceName: "fork_task_ref", + inputParameters: {}, + type: TaskType.FORK_JOIN, + }, + required: [ + "name", + "taskReferenceName", + "inputParameters", + "type", + "forkTasks", + ], + properties: { + name: { $ref: nameSchema.$id }, + taskReferenceName: { $ref: taskReferenceName.$id }, + inputParameters: { + $ref: inputParameters.$id, + }, + forkTasks: { + type: "array", + default: [[]], + items: { + $ref: "/properties/tasks", + }, + }, + type: { const: TaskType.FORK_JOIN }, + }, + additionalProperties: true, +}; + +export const waitSchema = { + $id: "/properties/tasks/wait", + type: "object", + description: "Wait task", + default: { + name: "wait_ref", + taskReferenceName: "wait_ref", + type: TaskType.WAIT, + }, + required: ["name", "taskReferenceName", "type"], + properties: { + name: { $ref: nameSchema.$id }, + taskReferenceName: { $ref: taskReferenceName.$id }, + inputParameters: { + $ref: inputParameters.$id, + }, + type: { const: TaskType.WAIT }, + }, + additionalProperties: true, +}; + +export const forkJoinDynamicSchema = { + $id: "/properties/tasks/forkJoinDynamic", + type: "object", + description: "Fork join dynamic task", + default: { + name: "fork_join_dynamic_ref", + taskReferenceName: "fork_join_dynamic_ref", + inputParameters: {}, + type: TaskType.FORK_JOIN_DYNAMIC, + }, + required: ["name", "taskReferenceName", "type", "inputParameters"], + properties: { + inputParameters: { + $ref: inputParameters.$id, + }, + dynamicForkTasksParam: { + type: "string", + }, + dynamicForkTasksInputParamName: { + type: "string", + }, + name: { $ref: nameSchema.$id }, + taskReferenceName: { $ref: taskReferenceName.$id }, + type: { const: TaskType.FORK_JOIN_DYNAMIC }, + }, + additionalProperties: true, +}; + +export const dynamicTaskSchema = { + $id: "/properties/tasks/dynamic", + type: "object", + description: "Dynamic task", + default: { + name: "dynamic_ref", + taskReferenceName: "dynamic_ref", + type: TaskType.DYNAMIC, + }, + required: ["name", "taskReferenceName", "type"], + properties: { + inputParameters: { + $ref: inputParameters.$id, + }, + dynamicTaskNameParam: { + type: "string", + }, + name: { $ref: nameSchema.$id }, + taskReferenceName: { $ref: taskReferenceName.$id }, + type: { const: TaskType.DYNAMIC }, + }, + additionalProperties: true, +}; + +export const inlineTaskSchema = { + $id: "/properties/tasks/inlineSchema", + type: "object", + description: "Inline task schema", + default: { + name: "inline_task_ref", + taskReferenceName: "inline_task_ref", + type: TaskType.INLINE, + }, + required: ["name", "taskReferenceName", "type"], + properties: { + inputParameters: { + anyOf: [ + { + type: "object", + required: ["evaluatorType", "expression"], + properties: { + evaluatorType: { + type: "string", + enum: ["javascript", "graaljs"], + }, + expression: { + type: "string", + }, + additionalProperties: true, + }, + }, + { + type: "string", + pattern: variablePattern, + }, + ], + }, + name: { $ref: nameSchema.$id }, + taskReferenceName: { $ref: taskReferenceName.$id }, + type: { const: TaskType.INLINE }, + }, + additionalProperties: true, +}; + +export const switchTaskSchema = { + $id: "/properties/tasks/switchTaskSchema", + type: "object", + description: "Switch task schema", + default: { + name: "switch_task_ref", + taskReferenceName: "switch_task_ref", + type: TaskType.SWITCH, + }, + required: [ + "name", + "taskReferenceName", + "type", + "evaluatorType", + "expression", + "decisionCases", + "defaultCase", + ], + properties: { + inputParameters: { + $ref: inputParameters.$id, + }, + name: { $ref: nameSchema.$id }, + taskReferenceName: { $ref: taskReferenceName.$id }, + type: { const: TaskType.SWITCH }, + evaluatorType: { + enum: ["javascript", "value-param", "graaljs"], + type: "string", + }, + expression: { + type: "string", + }, + decisionCases: { + type: "object", + patternProperties: { + ".*": { + $ref: "/properties/tasks", + }, + }, + additionalProperties: true, + }, + defaultCase: { + $ref: "/properties/tasks", + }, + }, + additionalProperties: true, +}; + +export const kafkaRequestTaskSchema = { + $id: "/properties/tasks/kafkaRequestSchema", + type: "object", + description: "Kafka task", + default: { + name: "http_task_ref", + taskReferenceName: "http_task_ref", + type: TaskType.KAFKA_PUBLISH, + }, + required: ["name", "taskReferenceName", "type", "inputParameters"], + properties: { + inputParameters: { + anyOf: [ + { + type: "object", + properties: { + kafka_request: { + anyOf: [ + { + type: "object", + properties: { + headers: { + anyOf: [ + { + type: "object", + }, + { + type: "string", + pattern: variablePattern, + }, + ], + }, + key: { + type: "string", + }, + value: { + anyOf: [ + { + type: "object", + }, + { + type: "string", + }, + { + type: "number", + }, + { + type: "boolean", + }, + { + type: "array", + }, + { + type: "null", + }, + ], + }, + }, + additionalProperties: true, + }, + { + type: "string", + pattern: variablePattern, + }, + ], + }, + }, + required: ["kafka_request"], + additionalProperties: true, + }, + { + type: "string", + pattern: variablePattern, + }, + ], + }, + name: { $ref: nameSchema.$id }, + taskReferenceName: { $ref: taskReferenceName.$id }, + type: { const: TaskType.KAFKA_PUBLISH }, + }, + additionalProperties: true, +}; + +export const baseHTTPRequestSchema = { + $id: "/properties/tasks/properties/inputParameters/properties/http_request", + type: "object", + properties: { + uri: { + type: "string", + }, + method: { + type: "string", + anyOf: [ + { + enum: Object.values(HTTPMethods), + }, + { + pattern: variablePattern, + }, + ], + }, + headers: { + type: ["string", "object"], + patternProperties: { + "^\\S*$": { type: "string" }, + }, + additionalProperties: false, + }, + terminationCondition: { + type: "string", + }, + pollingInterval: { + type: "string", + }, + pollingStrategy: { + type: "string", + }, + encode: { + type: "boolean", + }, + additionalProperties: true, + }, + + additionalProperties: true, +}; + +export const httpTaskSchema = { + $id: "/properties/tasks/httpTaskSchema", + type: "object", + description: "HTTP task", + default: { + name: "http_task_ref", + taskReferenceName: "http_task_ref", + type: TaskType.HTTP, + }, + required: ["name", "taskReferenceName", "type", "inputParameters"], + properties: { + inputParameters: { + type: ["object", "string"], + properties: { + http_request: { + anyOf: [ + { + $ref: baseHTTPRequestSchema.$id, + }, + { + type: "string", + pattern: variablePattern, + }, + ], + }, + }, + additionalProperties: true, + }, + name: { $ref: nameSchema.$id }, + taskReferenceName: { $ref: taskReferenceName.$id }, + type: { const: TaskType.HTTP }, + }, + additionalProperties: true, +}; + +export const httpPollTaskSchema = { + $id: "/properties/tasks/httpPollTaskSchema", + type: "object", + description: "HTTP POLL task", + default: { + name: "http_poll_task_ref", + taskReferenceName: "http_poll_task_ref", + type: TaskType.HTTP_POLL, + }, + required: ["name", "taskReferenceName", "type", "inputParameters"], + properties: { + inputParameters: { + type: ["object", "string"], + properties: { + http_request: { + anyOf: [ + { + type: "object", + $ref: baseHTTPRequestSchema.$id, + required: [ + "terminationCondition", + "pollingInterval", + "pollingStrategy", + ], + }, + { + type: "string", + pattern: variablePattern, + }, + ], + }, + }, + required: ["http_request"], + additionalProperties: true, + }, + name: { $ref: nameSchema.$id }, + taskReferenceName: { $ref: taskReferenceName.$id }, + type: { const: TaskType.HTTP_POLL }, + }, + additionalProperties: true, +}; + +export const jsonJQTaskSchema = { + $id: "/properties/tasks/jsonJQTask", + type: "object", + description: "JsonJQTask task", + default: { + name: "join_jq_task_ref", + taskReferenceName: "join_jq_task_ref", + type: TaskType.JSON_JQ_TRANSFORM, + }, + required: ["name", "taskReferenceName", "type", "inputParameters"], + properties: { + inputParameters: { + anyOf: [ + { + type: "object", + properties: { + queryExpression: { + type: "string", + }, + }, + required: ["queryExpression"], + additionalProperties: true, + }, + { + type: "string", + pattern: variablePattern, + }, + ], + }, + name: { $ref: nameSchema.$id }, + taskReferenceName: { $ref: taskReferenceName.$id }, + type: { const: TaskType.JSON_JQ_TRANSFORM }, + }, + additionalProperties: true, +}; + +export const terminateTaskSchema = { + $id: "/properties/tasks/terminate", + type: "object", + description: "Terminate Task", + default: { + name: "terminate_ref", + taskReferenceName: "terminate_ref", + type: TaskType.TERMINATE, + }, + required: ["name", "taskReferenceName", "type"], + properties: { + inputParameters: { + anyOf: [ + { + type: "object", + properties: { + terminationStatus: { + enum: ["COMPLETED", "FAILED", "TERMINATED"], + type: "string", + }, + workflowOutput: { + type: "object", + }, + }, + additionalProperties: true, + required: ["terminationStatus"], + }, + { + type: "string", + pattern: variablePattern, + }, + ], + }, + name: { $ref: nameSchema.$id }, + taskReferenceName: { $ref: taskReferenceName.$id }, + type: { const: TaskType.TERMINATE }, + }, + additionalProperties: true, +}; + +export const setVariableTaskSchema = { + $id: "/properties/tasks/setVariable", + type: "object", + description: "Set variable", + default: { + name: "set_variable_ref", + taskReferenceName: "set_variable_ref", + type: TaskType.SET_VARIABLE, + }, + required: ["name", "taskReferenceName", "type", "inputParameters"], + properties: { + inputParameters: { + $ref: inputParameters.$id, + }, + name: { $ref: nameSchema.$id }, + taskReferenceName: { $ref: taskReferenceName.$id }, + type: { const: TaskType.SET_VARIABLE }, + }, + additionalProperties: true, +}; + +export const terminateWorkflowSchema = { + $id: "/properties/tasks/terminateWorkflowSchema", + type: "object", + description: "Terminate workflow", + default: { + name: "terminate_workflow_schema_ref", + taskReferenceName: "terminate_workflow_schema_ref", + inputParameters: { + workflowId: "someWorkflowId", + }, + type: TaskType.TERMINATE_WORKFLOW, + }, + required: ["name", "taskReferenceName", "inputParameters", "type"], + properties: { + name: { $ref: nameSchema.$id }, + taskReferenceName: { $ref: taskReferenceName.$id }, + inputParameters: { + anyOf: [ + { + type: "object", + required: ["workflowId"], + properties: { + workflowId: { + anyOf: [ + { + type: "string", + }, + { + type: "array", + items: { + type: "string", + }, + }, + ], + }, + terminationReason: { + type: "string", + }, + }, + additionalProperties: true, + }, + { + type: "string", + pattern: variablePattern, + }, + ], + }, + type: { const: TaskType.TERMINATE_WORKFLOW }, + }, + additionalProperties: true, +}; + +export const businessRuleSchema = { + $id: "/properties/tasks/businessRuleSchema", + type: "object", + description: "Evaluate business rules that are compiled in a spreadsheet.", + default: { + name: "business_rule_schema_ref", + taskReferenceName: "business_rule_schema_ref", + inputParameters: { + ruleFileLocation: "https://business-rules.s3.amazonaws.com/rules.xlsx", + executionStrategy: "FIRE_FIRST", + inputColumns: {}, + outputColumns: [], + }, + type: TaskType.BUSINESS_RULE, + }, + required: ["name", "taskReferenceName", "inputParameters", "type"], + properties: { + name: { $ref: nameSchema.$id }, + taskReferenceName: { $ref: taskReferenceName.$id }, + inputParameters: { + anyOf: [ + { + type: "object", + properties: { + inputColumns: { + anyOf: [ + { + type: "object", + additionalProperties: true, + }, + { + type: "string", + pattern: variablePattern, + }, + ], + }, + outputColumns: { + anyOf: [ + { + type: "array", + items: { type: "string" }, + }, + { + type: "string", + pattern: variablePattern, + }, + ], + }, + additionalProperties: true, + }, + }, + { + type: "string", + pattern: variablePattern, + }, + ], + }, + type: { const: TaskType.BUSINESS_RULE }, + }, + additionalProperties: true, +}; + +export const sendgridMailRequestSchema = { + $id: "/properties/tasks/sendgridSchema/properties/inputParameters", + type: "object", + required: ["from", "to", "contentType", "content", "sendgridConfiguration"], + properties: { + from: { + type: "string", + }, + to: { + type: "string", + }, + subject: { + type: "string", + }, + contentType: { + type: "string", + }, + content: { + type: "string", + }, + sendgridConfiguration: { + type: "string", + }, + }, + + additionalProperties: true, +}; + +export const sendgridSchema = { + $id: "/properties/tasks/sendgridSchema", + type: "object", + description: "Send email using sendgrid", + default: { + name: "sendgrid_task_ref", + taskReferenceName: "sendgrid_task_ref", + type: TaskType.SENDGRID, + }, + required: ["name", "taskReferenceName", "type", "inputParameters"], + properties: { + name: { $ref: nameSchema.$id }, + taskReferenceName: { $ref: taskReferenceName.$id }, + type: { const: TaskType.SENDGRID }, + inputParameters: { $ref: sendgridMailRequestSchema.$id }, + additionalProperties: true, + }, +}; + +export const subWorkflowTaskSchema = { + $id: "/properties/tasks/subWorkflowTask", + type: "object", + description: "sub workflow variable", + default: { + name: "sub_workflow_ref", + taskReferenceName: "sub_workflow_ref", + type: TaskType.SUB_WORKFLOW, + }, + required: [ + "name", + "taskReferenceName", + "type", + "inputParameters", + "subWorkflowParam", + ], + properties: { + inputParameters: { + $ref: inputParameters.$id, + }, + name: { $ref: nameSchema.$id }, + taskReferenceName: { $ref: taskReferenceName.$id }, + subWorkflowParam: { + type: "object", + properties: { + name: { + type: "string", + }, + version: { + type: ["integer", "string"], + }, + taskToDomain: { + type: "object", + additionalProperties: true, + }, + workflowDefinition: { + anyOf: [ + { + type: "string", + }, + { + $ref: "/workflow-schema", + }, + ], + }, + }, + }, + type: { const: TaskType.SUB_WORKFLOW }, + }, + additionalProperties: true, +}; + +export const startWorkflowTaskSchema = { + $id: "/properties/tasks/starkWorkflow", + type: "object", + description: "Start Workflow", + default: { + name: "start_workflow", + taskReferenceName: "start_workflow_ref", + inputParameters: { + startWorkflow: { + name: "image_convert_resize", + input: {}, + }, + }, + type: TaskType.START_WORKFLOW, + }, + required: ["name", "taskReferenceName", "type", "inputParameters"], + properties: { + name: { $ref: nameSchema.$id }, + taskReferenceName: { $ref: taskReferenceName.$id }, + inputParameters: { + $ref: inputParameters.$id, + }, + type: { const: TaskType.START_WORKFLOW }, + }, +}; + +export const webhookTaskSchema = { + $id: "/properties/tasks/webhook", + type: "object", + description: "Wait For Webhook Task", + default: { + name: "webhook", + taskReferenceName: "webhook_ref", + inputParameters: { + matches: { + type: "object", + }, + }, + type: TaskType.WAIT_FOR_WEBHOOK, + required: ["matches"], + }, + required: ["name", "taskReferenceName", "type", "inputParameters"], + properties: { + name: { $ref: nameSchema.$id }, + taskReferenceName: { $ref: taskReferenceName.$id }, + inputParameters: { + anyOf: [ + { + type: "object", + properties: { + matches: { + anyOf: [ + { + type: "object", + additionalProperties: true, + }, + { + type: "string", + pattern: variablePattern, + }, + ], + }, + }, + }, + { + type: "string", + pattern: variablePattern, + }, + ], + }, + type: { const: TaskType.WAIT_FOR_WEBHOOK }, + }, +}; + +export const humanTaskSchema = { + $id: "/properties/tasks/human", + type: "object", + description: "Will wait for human interaction", + default: { + name: "human_name", + taskReferenceName: "human_ref", + type: TaskType.HUMAN, + }, + required: ["name", "taskReferenceName", "type"], + properties: { + name: { $ref: nameSchema.$id }, + taskReferenceName: { $ref: taskReferenceName.$id }, + type: { const: TaskType.HUMAN }, + inputParameters: { + anyOf: [ + { + type: "object", + properties: { + _humanTaskTemplate: { + type: "string", + }, + _humanTaskAssignmentPolicy: { + anyOf: [ + { + type: "object", + properties: { + type: { + type: "string", + }, + subjects: { + type: ["string", "array"], + items: { + type: "string", + }, + }, + groupId: { + type: "string", + }, + }, + }, + { + type: "string", + pattern: variablePattern, + }, + ], + }, + _humanTaskTimeoutPolicy: { + anyOf: [ + { + type: "object", + properties: { + type: { + type: "string", + }, + timeoutSeconds: { + type: "integer", + }, + subjects: { + type: ["string", "array"], + items: { + type: "string", + }, + }, + }, + }, + { + type: "string", + pattern: variablePattern, + }, + ], + }, + additionalProperties: true, + }, + }, + { + type: "string", + pattern: variablePattern, + }, + ], + }, + }, + additionalProperties: true, +}; + +export const jdbcTaskSchema = { + $id: "/properties/tasks/jdbcTaskSchema", + type: "object", + description: "JDBC task", + default: { + name: "jdbc_task_ref", + taskReferenceName: "jdbc_task_ref", + type: TaskType.JDBC, + inputParameters: { + connectionId: "", // TODO: will be deprecated + integrationName: "", + statement: "SELECT * FROM tableName WHERE id=?", + parameters: [], + expectedUpdateCount: 0, + jdbcType: JDBCType.SELECT, + }, + }, + required: ["name", "taskReferenceName", "inputParameters", "type"], + properties: { + name: { $ref: nameSchema.$id }, + taskReferenceName: { $ref: taskReferenceName.$id }, + inputParameters: { + type: ["object", "string"], + properties: { + connectionId: { + type: "string", + }, + integrationName: { type: "string" }, + statement: { + type: "string", + }, + parameters: { + anyOf: [ + { + type: "array", + }, + { + type: "string", + pattern: variablePattern, + }, + ], + }, + expectedUpdateCount: { + type: "string", + }, + type: { + type: "string", + enum: Object.values(JDBCType), + }, + }, + required: ["integrationName", "statement", "type"], + additionalProperties: true, + }, + type: { const: TaskType.JDBC }, + }, + additionalProperties: true, +}; + +export const updateSecretTaskSchema = { + $id: "/properties/tasks/updateSecretTaskSchema", + type: "object", + description: "Update secret task", + default: { + name: "update_secret_task_ref", + taskReferenceName: "update_secret_task_ref", + type: TaskType.UPDATE_SECRET, + inputParameters: { + _secrets: { + secretKey: "my_token", + secretValue: "token value", + }, + }, + }, + required: ["name", "taskReferenceName", "inputParameters", "type"], + properties: { + name: { $ref: nameSchema.$id }, + taskReferenceName: { $ref: taskReferenceName.$id }, + inputParameters: { + anyOf: [ + { + type: "object", + properties: { + _secrets: { + anyOf: [ + { + type: "object", + properties: { + secretKey: { + type: "string", + }, + secretValue: { + type: "string", + }, + }, + required: ["secretKey", "secretValue"], + }, + { + type: "string", + pattern: variablePattern, + }, + ], + }, + }, + required: ["_secrets"], + additionalProperties: false, + }, + { + type: "string", + pattern: variablePattern, + }, + ], + }, + type: { const: TaskType.UPDATE_SECRET }, + }, + additionalProperties: true, +}; + +export const queryProcessorTaskSchema = { + $id: "/properties/tasks/queryProcessorTaskSchema", + type: "object", + description: "Query processor task", + default: { + name: "query_processor_task_ref", + taskReferenceName: "query_processor_task_ref", + type: TaskType.QUERY_PROCESSOR, + inputParameters: { + workflowNames: [], + statuses: [], + queryType: QueryProcessorType.CONDUCTOR_API, + }, + }, + required: ["name", "taskReferenceName", "inputParameters", "type"], + properties: { + name: { $ref: nameSchema.$id }, + taskReferenceName: { $ref: taskReferenceName.$id }, + inputParameters: { + type: ["object", "string"], + properties: { + workflowNames: { + anyOf: [ + { + type: "array", + }, + { + type: "string", + pattern: variablePattern, + }, + ], + }, + statuses: { + anyOf: [ + { + type: "array", + }, + { + type: "string", + pattern: variablePattern, + }, + ], + }, + queryType: { + type: "string", + enum: Object.values(QueryProcessorType), + }, + }, + required: ["queryType"], + additionalProperties: true, + }, + type: { const: TaskType.QUERY_PROCESSOR }, + }, + additionalProperties: true, +}; + +export const getSignedJwtTaskSchema = { + $id: "/properties/tasks/getSignedJwtTaskSchema", + type: "object", + description: "Get signed JWT task", + default: { + name: "get_signed_jwt_task_ref", + taskReferenceName: "get_signed_jwt_task_ref", + type: TaskType.GET_SIGNED_JWT, + inputParameters: { + subject: "", + issuer: "", + privateKey: "", + privateKeyId: "", + audience: "", + ttlInSecond: 0, + scopes: [], + algorithm: GetSignedJWTAlgorithmType.RS256, + }, + }, + required: ["name", "taskReferenceName", "inputParameters", "type"], + properties: { + name: { $ref: nameSchema.$id }, + taskReferenceName: { $ref: taskReferenceName.$id }, + inputParameters: { + type: ["object", "string"], + properties: { + subject: { + type: "string", + }, + issuer: { + type: "string", + }, + privateKey: { + type: "string", + }, + privateKeyId: { + type: "string", + }, + audience: { + type: "string", + }, + ttlInSecond: { + anyOf: [ + { + type: "number", + }, + { + type: "string", + pattern: ".*\\$\\{.*", + }, + ], + }, + scopes: { + anyOf: [ + { + type: "array", + items: { + type: "string", + }, + }, + { + type: "string", + pattern: ".*\\$\\{.*", + }, + ], + }, + algorithm: { + type: "string", + enum: Object.values(GetSignedJWTAlgorithmType), + }, + }, + required: [], + additionalProperties: true, + }, + type: { const: TaskType.GET_SIGNED_JWT }, + }, + additionalProperties: true, +}; + +export const opsGenieTaskSchema = { + $id: "/properties/tasks/opsGenieTaskSchema", + type: "object", + description: "Ops genie task", + default: { + name: "ops_genie_task_ref", + taskReferenceName: "ops_genie_task_ref", + type: TaskType.OPS_GENIE, + inputParameters: {}, + }, + required: ["name", "taskReferenceName", "inputParameters", "type"], + properties: { + name: { $ref: nameSchema.$id }, + taskReferenceName: { $ref: taskReferenceName.$id }, + inputParameters: { + type: ["object", "string"], + properties: { + alias: { + type: "string", + }, + description: { + type: "string", + }, + message: { + type: "string", + }, + }, + required: [], + additionalProperties: true, + }, + type: { const: TaskType.OPS_GENIE }, + }, + additionalProperties: true, +}; + +export const updateTaskSchema = { + $id: "/properties/tasks/updateTask", + type: "object", + description: "Update task", + default: { + name: "update_task_ref", + taskReferenceName: "update_task_ref", + type: TaskType.UPDATE_TASK, + inputParameters: {}, + }, + required: [], + properties: { + name: { $ref: nameSchema.$id }, + taskReferenceName: { $ref: taskReferenceName.$id }, + inputParameters: { + type: ["object", "string"], + properties: { + taskStatus: { + anyOf: [ + { + type: "string", + enum: Object.values(UpdateTaskStatus), + }, + { + type: "string", + pattern: variablePattern, + }, + ], + }, + taskRefName: { + type: "string", + }, + workflowId: { + type: "string", + }, + taskId: { + type: "string", + }, + taskOutput: { + type: "object", + }, + mergeOutput: { + type: "boolean", + }, + }, + additionalProperties: true, + }, + type: { const: TaskType.UPDATE_TASK }, + }, + additionalProperties: true, +}; + +export const getWorkflowSchema = { + $id: "/properties/tasks/getWorkflowSchema", + type: "object", + description: "Get Workflow", + default: { + name: "get_workflow_task_ref", + taskReferenceName: "get_workflow_task_ref", + type: TaskType.UPDATE_TASK, + }, + required: ["name", "taskReferenceName", "type", "inputParameters"], + properties: { + name: { $ref: nameSchema.$id }, + taskReferenceName: { $ref: taskReferenceName.$id }, + type: { const: TaskType.GET_WORKFLOW }, + workflowId: { + type: "string", + }, + includeTasks: { + type: "boolean", + }, + }, + additionalProperties: true, +}; + +export const chunkTextTaskSchema = { + $id: "/properties/tasks/chunkTextTaskSchema", + type: "object", + description: "Chunk text task", + default: { + name: "chunk_text_task_ref", + taskReferenceName: "chunk_text_task_ref", + type: TaskType.CHUNK_TEXT, + }, + properties: { + name: { $ref: nameSchema.$id }, + taskReferenceName: { $ref: taskReferenceName.$id }, + inputParameters: { + type: ["object", "string"], + properties: { + text: { + type: "string", + }, + chunkSize: { + type: "number", + }, + mediaType: { + type: "string", + }, + }, + required: [], + additionalProperties: true, + }, + type: { const: TaskType.CHUNK_TEXT }, + }, +}; + +export const listFilesTaskSchema = { + $id: "/properties/tasks/listFilesTaskSchema", + type: "object", + description: "List Files task", + default: { + name: "list_files_task_ref", + taskReferenceName: "list_files_task_ref", + type: TaskType.LIST_FILES, + }, + required: ["name", "taskReferenceName", "type", "inputParameters"], + properties: { + name: { $ref: nameSchema.$id }, + taskReferenceName: { $ref: taskReferenceName.$id }, + type: { const: TaskType.LIST_FILES }, + inputParameters: { + type: "object", + required: ["inputLocation"], + properties: { + inputLocation: { + type: "string", + }, + integrationName: { + type: "string", + }, + outputLocation: { + type: "string", + }, + fileTypes: { + type: "array", + items: { + type: "string", + }, + }, + integrationNames: { + type: "object", + additionalProperties: { + type: "string", + }, + }, + }, + additionalProperties: true, + }, + }, +}; + +export const parseDocumentTaskSchema = { + $id: "/properties/tasks/parseDocumentTaskSchema", + type: "object", + description: "Parse document", + default: { + name: "parse_document_task_ref", + taskReferenceName: "parse_document_task_ref", + type: TaskType.PARSE_DOCUMENT, + }, + required: [], + properties: { + name: { $ref: nameSchema.$id }, + taskReferenceName: { $ref: taskReferenceName.$id }, + type: { const: TaskType.PARSE_DOCUMENT }, + integrationName: { + type: "string", + }, + url: { + type: "string", + }, + mediaType: { + type: "string", + }, + chunkSize: { + type: "number", + }, + }, + additionalProperties: true, +}; + +export const tasksItemsSchema = { + $id: "/properties/tasks", + type: "array", + title: "Workflow Tasks", + description: "This list holds the tasks for your workflow.", + default: [], + items: { + $id: "/properties/tasks/items", + oneOf: [ + { $ref: simpleTaskSchema.$id }, + { $ref: yieldTaskSchema.$id }, + { $ref: doWhileSchema.$id }, + { $ref: forkTaskSchema.$id }, + { $ref: joinTaskSchema.$id }, + { $ref: waitSchema.$id }, + { $ref: forkJoinDynamicSchema.$id }, + { $ref: dynamicTaskSchema.$id }, + { $ref: terminateTaskSchema.$id }, + { $ref: setVariableTaskSchema.$id }, + { $ref: subWorkflowTaskSchema.$id }, + { $ref: jsonJQTaskSchema.$id }, + { $ref: httpTaskSchema.$id }, + { $ref: switchTaskSchema.$id }, + { $ref: inlineTaskSchema.$id }, + { $ref: eventTaskSchema.$id }, + { $ref: kafkaRequestTaskSchema.$id }, + { $ref: terminateWorkflowSchema.$id }, + { $ref: genericSchema.$id }, + { $ref: businessRuleSchema.$id }, + { $ref: sendgridSchema.$id }, + { $ref: humanTaskSchema.$id }, + { $ref: startWorkflowTaskSchema.$id }, + { $ref: httpPollTaskSchema.$id }, + { $ref: webhookTaskSchema.$id }, + { $ref: jdbcTaskSchema.$id }, + { $ref: updateSecretTaskSchema.$id }, + { $ref: queryProcessorTaskSchema.$id }, + { $ref: opsGenieTaskSchema.$id }, + { $ref: getSignedJwtTaskSchema.$id }, + { $ref: updateTaskSchema.$id }, + { $ref: getWorkflowSchema.$id }, + { $ref: chunkTextTaskSchema.$id }, + { $ref: listFilesTaskSchema.$id }, + { $ref: parseDocumentTaskSchema.$id }, + ], + }, +}; + +export const schemasByType = { + [TaskType.SIMPLE]: simpleTaskSchema, + [TaskType.YIELD]: yieldTaskSchema, + [TaskType.DO_WHILE]: doWhileSchema, + [TaskType.FORK_JOIN]: forkTaskSchema, + [TaskType.WAIT]: waitSchema, + [TaskType.FORK_JOIN_DYNAMIC]: forkJoinDynamicSchema, + [TaskType.DYNAMIC]: dynamicTaskSchema, + [TaskType.TERMINATE]: terminateTaskSchema, + [TaskType.SET_VARIABLE]: setVariableTaskSchema, + [TaskType.SUB_WORKFLOW]: subWorkflowTaskSchema, + [TaskType.JSON_JQ_TRANSFORM]: jsonJQTaskSchema, + [TaskType.HTTP]: httpTaskSchema, + [TaskType.SWITCH]: switchTaskSchema, + [TaskType.INLINE]: inlineTaskSchema, + [TaskType.JOIN]: joinTaskSchema, + [TaskType.EVENT]: eventTaskSchema, + [TaskType.KAFKA_PUBLISH]: kafkaRequestTaskSchema, + [TaskType.TERMINATE_WORKFLOW]: terminateWorkflowSchema, + [TaskType.BUSINESS_RULE]: businessRuleSchema, + [TaskType.SENDGRID]: sendgridSchema, + [TaskType.START]: genericSchema, + [TaskType.DECISION]: genericSchema, + [TaskType.USER_DEFINED]: genericSchema, + [TaskType.LAMBDA]: genericSchema, + [TaskType.EXCLUSIVE_JOIN]: genericSchema, + [TaskType.TERMINAL]: genericSchema, + [TaskType.HUMAN]: humanTaskSchema, + [TaskType.TASK_SUMMARY]: genericSchema, + [TaskType.WAIT_FOR_EVENT]: genericSchema, + [TaskType.WAIT_FOR_WEBHOOK]: webhookTaskSchema, + [TaskType.START_WORKFLOW]: startWorkflowTaskSchema, + [TaskType.HTTP_POLL]: httpPollTaskSchema, + [TaskType.JDBC]: jdbcTaskSchema, + [TaskType.IA_TASK]: genericSchema, + [TaskType.SWITCH_JOIN]: genericSchema, // Pseudo task does not really exist + [TaskType.LLM_TEXT_COMPLETE]: genericSchema, + [TaskType.LLM_GENERATE_EMBEDDINGS]: genericSchema, + [TaskType.LLM_GET_EMBEDDINGS]: genericSchema, + [TaskType.LLM_STORE_EMBEDDINGS]: genericSchema, + [TaskType.LLM_SEARCH_INDEX]: genericSchema, + [TaskType.LLM_INDEX_DOCUMENT]: genericSchema, + [TaskType.GET_DOCUMENT]: genericSchema, + [TaskType.LLM_CHAT_COMPLETE]: genericSchema, + [TaskType.LLM_INDEX_TEXT]: genericSchema, + [TaskType.UPDATE_SECRET]: updateSecretTaskSchema, + [TaskType.JUMP]: genericSchema, + [TaskType.QUERY_PROCESSOR]: queryProcessorTaskSchema, + [TaskType.OPS_GENIE]: opsGenieTaskSchema, + [TaskType.GET_SIGNED_JWT]: getSignedJwtTaskSchema, + [TaskType.UPDATE_TASK]: updateTaskSchema, + [TaskType.GET_WORKFLOW]: getWorkflowSchema, + [TaskType.GRPC]: genericSchema, + [TaskType.MCP]: genericSchema, + [TaskType.MCP_REMOTE]: genericSchema, + [TaskType.CHUNK_TEXT]: chunkTextTaskSchema, + [TaskType.LIST_FILES]: listFilesTaskSchema, + [TaskType.PARSE_DOCUMENT]: parseDocumentTaskSchema, +}; + +// Object.values(TaskType) +export const workflowSchema = { + $id: "/workflow-schema", + required: ["name", "version", "tasks", "schemaVersion"], + type: "object", + properties: { + name: { + $id: "/properties/name", + default: "", + description: WORKFLOW_NAME_ERROR_MESSAGE, + maxLength: 100, + pattern: regexToString(WORKFLOW_NAME_REGEX), + title: "Workflow Name", + type: "string", + }, + description: { + $id: "/properties/description", + type: "string", + title: "Workflow Description", + description: "An brief description of your workflow for reference.", + default: "", + }, + version: { + $id: "/properties/version", + default: 0, + description: "An explanation about the purpose of this instance.", + title: "The version schema", + minimum: 1, + type: "integer", + }, + tasks: { + $ref: tasksItemsSchema.$id, + }, + inputParameters: { + $id: "/properties/inputParameters", + type: "array", + title: "Workflow Input Parameters", + description: "An explanation about the purpose of this instance.", + default: [], + examples: [[]], + items: { + $id: "/properties/inputParameters/items", + }, + }, + outputParameters: { + $id: "/properties/outputParameters", + type: "object", + title: "The outputParameters schema", + description: "An explanation about the purpose of this instance.", + default: {}, + required: [], + properties: {}, + additionalProperties: true, + }, + schemaVersion: { + $id: "/properties/schemaVersion", + type: "integer", + title: "Schema Version", + description: "Fixed schema version", + default: 2, + }, + restartable: { + $id: "/properties/restartable", + type: "boolean", + title: "Workflow restartable", + description: "Specify if the workflow is restartable.", + default: true, + }, + workflowStatusListenerEnabled: { + $id: "/properties/workflowStatusListenerEnabled", + type: "boolean", + title: "The workflowStatusListenerEnabled schema", + description: "An explanation about the purpose of this instance.", + default: false, + }, + ownerEmail: { + $id: "/properties/ownerEmail", + type: "string", + title: "The ownerEmail schema", + description: "An explanation about the purpose of this instance.", + default: "", + }, + timeoutPolicy: { + $id: "/properties/timeoutPolicy", + type: "string", + enum: Object.values(TimeoutPolicy), + title: "The timeoutPolicy schema", + description: "An explanation about the purpose of this instance.", + default: "", + }, + timeoutSeconds: { + $id: "/properties/timeoutSeconds", + type: "integer", + title: "The timeoutSeconds schema", + description: "An explanation about the purpose of this instance.", + default: 0, + }, + failureWorkflow: { + $id: "/properties/failureWorkflow", + type: "string", + title: "Failure Workflow Name", + description: "Failure Workflow", + default: "", + }, + }, + additionalProperties: true, +}; + +export const workflowDefinitionSchemaWithDeps = [ + // Note that order matters here. + nameSchema, + taskReferenceName, + inputParameters, + simpleTaskSchema, + yieldTaskSchema, + doWhileSchema, + joinTaskSchema, + forkTaskSchema, + waitSchema, + forkJoinDynamicSchema, + dynamicTaskSchema, + terminateTaskSchema, + setVariableTaskSchema, + humanTaskSchema, + webhookTaskSchema, + subWorkflowTaskSchema, + terminateWorkflowSchema, + genericSchema, + jsonJQTaskSchema, + eventTaskSchema, + kafkaRequestTaskSchema, + businessRuleSchema, + sendgridMailRequestSchema, + sendgridSchema, + switchTaskSchema, + inlineTaskSchema, + baseHTTPRequestSchema, + httpTaskSchema, + httpPollTaskSchema, + startWorkflowTaskSchema, + tasksItemsSchema, + jdbcTaskSchema, + updateSecretTaskSchema, + queryProcessorTaskSchema, + opsGenieTaskSchema, + getSignedJwtTaskSchema, + updateTaskSchema, + getWorkflowSchema, + chunkTextTaskSchema, + listFilesTaskSchema, + parseDocumentTaskSchema, + // workflow must be at the end, because it wraps another schemas + workflowSchema, +]; diff --git a/ui-next/src/types/SchemasAjv.ts b/ui-next/src/types/SchemasAjv.ts new file mode 100644 index 0000000000..a8716522b5 --- /dev/null +++ b/ui-next/src/types/SchemasAjv.ts @@ -0,0 +1,47 @@ +import _dropRight from "lodash/dropRight"; +import _merge from "lodash/merge"; +import _set from "lodash/set"; +import { TimeoutPolicy } from "types/TimeoutPolicy"; +import { WORKFLOW_NAME_ERROR_MESSAGE } from "utils/constants/common"; +import { + nameSchema, + workflowDefinitionSchemaWithDeps, + workflowSchema, +} from "./Schemas"; + +export const nameSchemaAjv = _merge({}, nameSchema, { + errorMessage: { + pattern: WORKFLOW_NAME_ERROR_MESSAGE, + }, +}); + +// use 1st parameter as an empty object to prevent mutating the original object +export const workflowSchemaAjv = _merge({}, workflowSchema, { + properties: { + name: { + errorMessage: { + pattern: WORKFLOW_NAME_ERROR_MESSAGE, + maxLength: "name must less than 100 characters.", + }, + }, + timeoutPolicy: { + errorMessage: { + enum: `timeoutPolicy must be ${Object.values(TimeoutPolicy).join( + " | ", + )}.`, + }, + }, + }, +}); + +const schemaWithDepsAjv = _dropRight(workflowDefinitionSchemaWithDeps); + +// override nameSchema +_set(schemaWithDepsAjv, 0, nameSchemaAjv); + +export const workflowDefinitionSchemaWithDepsAjv = [ + // Note that order matters here. + ...schemaWithDepsAjv, + // workflow must be at the end, because it wraps another schemas + workflowSchemaAjv, +]; diff --git a/ui-next/src/types/Secret.ts b/ui-next/src/types/Secret.ts new file mode 100644 index 0000000000..0089be2e5c --- /dev/null +++ b/ui-next/src/types/Secret.ts @@ -0,0 +1,7 @@ +import { TagDto } from "./Tag"; + +export interface SecretDTO { + name: string; + value: string; + tags?: TagDto[]; +} diff --git a/ui-next/src/types/ServiceDefinition.ts b/ui-next/src/types/ServiceDefinition.ts new file mode 100644 index 0000000000..3115c0da12 --- /dev/null +++ b/ui-next/src/types/ServiceDefinition.ts @@ -0,0 +1,12 @@ +import { Tag } from "./common"; + +export type ServiceDefinition = { + name: string; + type: string; + location: string; + createdBy: string; + updatedBy: string; + createTime: number; + updateTime: number; + tags: Tag[]; +}; diff --git a/ui-next/src/types/Tag.ts b/ui-next/src/types/Tag.ts new file mode 100644 index 0000000000..ac1349ac1f --- /dev/null +++ b/ui-next/src/types/Tag.ts @@ -0,0 +1,2 @@ +import { Tag } from "./common"; +export type { Tag as TagDto }; diff --git a/ui-next/src/types/TaskDefinition.ts b/ui-next/src/types/TaskDefinition.ts new file mode 100644 index 0000000000..71b7252cf2 --- /dev/null +++ b/ui-next/src/types/TaskDefinition.ts @@ -0,0 +1,31 @@ +export type TaskDefinitionDto = { + backoffScaleFactor: number; + concurrentExecLimit?: number; + createTime: number; + createdBy: string; + description: string; + inputKeys: string[]; + inputTemplate: Record; + name: string; + outputKeys: string[]; + ownerEmail: string; + pollTimeoutSeconds: number; + rateLimitFrequencyInSeconds: number; + rateLimitPerFrequency: number; + responseTimeoutSeconds: number; + retryCount: number; + retryDelaySeconds: number; + retryLogic: string; + timeoutPolicy: string; + timeoutSeconds: number; + updateTime?: number; + updatedBy?: string; + inputSchema?: { + name: string; + version?: number; + }; + outputSchema?: { + name: string; + version?: number; + }; +}; diff --git a/ui-next/src/types/TaskExecution.ts b/ui-next/src/types/TaskExecution.ts new file mode 100644 index 0000000000..f78e49c434 --- /dev/null +++ b/ui-next/src/types/TaskExecution.ts @@ -0,0 +1,25 @@ +import { TaskType } from "types/common"; +import { TaskStatus } from "./TaskStatus"; + +export type TaskExecutionDto = { + workflowId: string; + workflowType: string; + scheduledTime: string; + startTime: string; + updateTime: string; + endTime: string; + status: TaskStatus; + executionTime: number; + queueWaitTime: number; + taskDefName: string; + taskType: TaskType; + input: string; + output: string; + taskId: string; + workflowPriority: number; +}; + +export type TaskExecutionResult = { + results: TaskExecutionDto[]; + totalHits: number; +}; diff --git a/ui-next/src/types/TaskLog.ts b/ui-next/src/types/TaskLog.ts new file mode 100644 index 0000000000..df13c5869d --- /dev/null +++ b/ui-next/src/types/TaskLog.ts @@ -0,0 +1,5 @@ +export interface TaskLog { + createdTime: number | string; + log: string; + taskId: string; +} diff --git a/ui-next/src/types/TaskStatus.ts b/ui-next/src/types/TaskStatus.ts new file mode 100644 index 0000000000..bf30210964 --- /dev/null +++ b/ui-next/src/types/TaskStatus.ts @@ -0,0 +1,15 @@ +export enum TaskStatus { + COMPLETED = "COMPLETED", + IN_PROGRESS = "IN_PROGRESS", + FAILED = "FAILED", + FAILED_WITH_TERMINAL_ERROR = "FAILED_WITH_TERMINAL_ERROR", + + SCHEDULED = "SCHEDULED", + CANCELED = "CANCELED", + COMPLETED_WITH_ERRORS = "COMPLETED_WITH_ERRORS", + TIMED_OUT = "TIMED_OUT", + SKIPPED = "SKIPPED", + + PENDING = "PENDING", + NULL = "NULL", +} diff --git a/ui-next/src/types/TaskType.ts b/ui-next/src/types/TaskType.ts new file mode 100644 index 0000000000..f0ac8ccfee --- /dev/null +++ b/ui-next/src/types/TaskType.ts @@ -0,0 +1,626 @@ +import { ExecutedData } from "./Execution"; +import { TaskDefinitionDto } from "./TaskDefinition"; +import { TaskType } from "./common"; + +// Copied and fixed from codegen. Use this one. +export enum PollingStrategy { + FIXED = "FIXED", + LINEAR_BACKOFF = "LINEAR_BACKOFF", + EXPONENTIAL_BACKOFF = "EXPONENTIAL_BACKOFF", +} + +// This is copy pasted from the SDK. We should probably use the SDK itself to hit the apis. + +export interface CommonTaskDef { + name: string; + taskReferenceName: string; + type: TaskType; + executionData?: Partial; + taskDefinition?: TaskDefinitionDto; + description?: string; + optional?: boolean; +} + +export interface JoinTaskDef extends CommonTaskDef { + type: TaskType.JOIN; + inputParameters?: Record; + joinOn: string[]; + evaluatorType?: string; // Will change when we get extra details + expression?: string; + asyncComplete?: boolean; +} + +export interface SwitchTaskDef extends CommonTaskDef { + inputParameters: Record; + type: TaskType.SWITCH; + decisionCases: Record; + defaultCase: CommonTaskDef[]; + evaluatorType: "value-param" | "javascript"; + expression: string; +} + +export enum HTTPMethods { + GET = "GET", + HEAD = "HEAD", + POST = "POST", + PUT = "PUT", + PATCH = "PATCH", + DELETE = "DELETE", + OPTIONS = "OPTIONS", + TRACE = "TRACE", +} + +export enum GRPCMethodTypes { + UNARY = "UNARY", + SERVER_STREAMING = "SERVER_STREAMING", + CLIENT_STREAMING = "CLIENT_STREAMING", + BIDIRECTIONAL_STREAMING = "BIDIRECTIONAL_STREAMING", +} + +export interface HttpInputParameters { + uri: string; + method: HTTPMethods | string; + accept?: string; + contentType?: string; + headers?: Record; + body?: unknown; + encode?: boolean; + hedgingConfig?: { maxAttempts?: number }; +} + +export interface HttpTaskDef extends CommonTaskDef { + inputParameters: + | (HttpInputParameters & { + http_request?: never; + }) + | { http_request?: HttpInputParameters }; + type: TaskType.HTTP; + asyncComplete?: boolean; + optional?: boolean; +} + +export interface GrpcTaskDef extends CommonTaskDef { + type: TaskType.GRPC; + inputParameters?: { + service?: string; + method?: string; + host?: string; + port?: number; + useSSL?: boolean; + trustCert?: boolean; + request?: Record; + headers?: Record; + inputType?: string; + methodType?: string; + outputType?: string; + hedgingConfig?: { maxAttempts?: number }; + }; +} + +export interface MCPTaskDef extends CommonTaskDef { + type: TaskType.MCP; + inputParameters?: { + service?: string; + method?: string; + useSSL?: boolean; + trustCert?: boolean; + request?: Record; + headers?: Record; + inputType?: string; + outputType?: string; + hedgingConfig?: { maxAttempts?: number }; + integrationType?: string; + }; +} + +export interface MCPRemoteTaskDef extends CommonTaskDef { + type: TaskType.MCP_REMOTE; + inputParameters?: { + method?: string; + url?: string; + request?: Record; + headers?: Record; + }; +} + +export interface HttpPollTaskDef extends CommonTaskDef { + inputParameters: { + [x: string]: unknown; + http_request: HttpInputParameters & { + pollingInterval: string; + pollingStrategy: PollingStrategy; + terminationCondition: string; + }; + }; + type: TaskType.HTTP_POLL; + asyncComplete?: boolean; +} + +export interface DoWhileTaskDef extends CommonTaskDef { + inputParameters: Record; + type: TaskType.DO_WHILE; + startDelay?: number; + optional?: boolean; + asyncComplete?: boolean; + loopCondition: string; + evaluatorType: string; + loopOver: CommonTaskDef[]; + keepLastN?: number; +} + +export interface SubWorkflowTaskDef extends CommonTaskDef { + type: TaskType.SUB_WORKFLOW; + inputParameters?: Record; + subWorkflowParam: { + name: string; + version?: number; + taskToDomain?: Record; + }; +} + +export interface TerminateTaskDef extends CommonTaskDef { + inputParameters: { + terminationStatus: "COMPLETED" | "FAILED"; + workflowOutput?: Record; + terminationReason?: string; + }; + type: TaskType.TERMINATE; + startDelay?: number; + optional?: boolean; +} +export interface ForkableTask extends CommonTaskDef { + forkTasks?: Array>; +} + +export interface ForkJoinTaskDef extends ForkableTask { + type: TaskType.FORK_JOIN; + inputParameters?: Record; + forkTasks: Array>; + defaultCase: Array; +} + +export interface ForkJoinDynamicDef extends ForkableTask { + inputParameters: { + dynamicTasks: any; + dynamicTasksInput: any; + }; + type: TaskType.FORK_JOIN_DYNAMIC; + dynamicForkTasksParam: string; // not string "dynamicTasks", + dynamicForkTasksInputParamName: string; // not string "dynamicTasksInput", + startDelay?: number; + optional?: boolean; + asyncComplete?: boolean; +} + +export interface EventTaskDef extends CommonTaskDef { + type: TaskType.EVENT; + sink: string; + asyncComplete?: boolean; + optional?: boolean; + inputParameters?: Record; +} + +export interface SimpleTaskDef extends CommonTaskDef { + type: TaskType.SIMPLE; + inputParameters?: Record; +} + +export interface YieldTaskDef extends CommonTaskDef { + type: TaskType.YIELD; + inputParameters?: Record; +} + +export interface ContainingQueryExpression { + queryExpression: string; + [x: string | number | symbol]: unknown; +} + +export interface JsonJQTransformTaskDef extends CommonTaskDef { + type: TaskType.JSON_JQ_TRANSFORM; + inputParameters: ContainingQueryExpression; +} + +export interface InlineTaskInputParameters { + evaluatorType: "javascript" | "graaljs"; + expression: string; + [x: string]: unknown; +} + +export interface InlineTaskDef extends CommonTaskDef { + type: TaskType.INLINE; + inputParameters: InlineTaskInputParameters; +} + +export interface KafkaPublishInputParameters { + topic: string; + value: string; + bootStrapServers: string; + headers: Record; + key: string | Record; + keySerializer: string; +} + +export interface KafkaPublishTaskDef extends CommonTaskDef { + inputParameters: { + kafka_request: KafkaPublishInputParameters; + }; + type: TaskType.KAFKA_PUBLISH; +} + +export interface DynamicInputParameters { + taskToExecute?: string; +} + +export interface DynamicTaskDef extends CommonTaskDef { + type: TaskType.DYNAMIC; + inputParameters: DynamicInputParameters; + dynamicTaskNameParam: string; +} + +export interface SetVariableTaskDef extends CommonTaskDef { + type: TaskType.SET_VARIABLE; + inputParameters: Record; +} + +interface TerminateWorkflowParameters { + workflowId: string[]; + terminationReason: string; + triggerFailureWorkflow: boolean; +} + +export interface TerminateWorkflowTaskDef extends CommonTaskDef { + type: TaskType.TERMINATE_WORKFLOW; + inputParameters: TerminateWorkflowParameters; +} + +interface BusinessRuleParameters { + ruleFileLocation: string; + executionStrategy: string; + inputColumns: string | Record; + outputColumns: string | any[]; + cacheTimeoutMinutes?: number; +} + +export interface BusinessRuleTaskDef extends CommonTaskDef { + type: TaskType.BUSINESS_RULE; + inputParameters: BusinessRuleParameters; +} + +interface SendgridParameters { + from: string; + to: string; + subject: string; + contentType: string; + content: string; + sendgridConfiguration: string; +} + +export interface SendgridTaskDef extends CommonTaskDef { + type: TaskType.SENDGRID; + inputParameters: SendgridParameters; +} + +export interface StartWorkflowInputParameters { + startWorkflow: { + name: string; + input: Record; + }; +} + +export interface StartWorkflowTaskDef extends CommonTaskDef { + inputParameters: StartWorkflowInputParameters; +} + +export interface WaitForWebHookTaskDef extends CommonTaskDef { + inputParameters: { + matches: Record | string; + }; +} + +export interface WaitTaskDef extends CommonTaskDef { + type: TaskType.WAIT; + inputParameters?: Record<"duration" | "until", string>; + optional?: boolean; +} + +export enum JDBCType { + SELECT = "SELECT", + UPDATE = "UPDATE", +} + +export enum QueryProcessorType { + CONDUCTOR_API = "CONDUCTOR_API", + METRICS = "METRICS", + CONDUCTOR_EVENTS = "CONDUCTOR_EVENTS", +} + +export enum GetSignedJWTAlgorithmType { + RS256 = "RS256", +} + +export interface JDBCInputParameters { + connectionId?: string; // TODO: will be deprecated + integrationName: string; + statement: string; + parameters: any[]; + expectedUpdateCount?: string; + type: JDBCType; +} + +export const jdbcParameterKeys: Array = [ + "connectionId", + "expectedUpdateCount", + "integrationName", + "parameters", + "statement", + "type", +]; + +export interface JDBCTaskDef extends CommonTaskDef { + type: TaskType.JDBC; + inputParameters: JDBCInputParameters; +} + +export interface UpdateSecretTaskDef extends CommonTaskDef { + type: TaskType.UPDATE_SECRET; + inputParameters: { + _secrets: { + secretKey: string; + secretValue: string; + }; + }; +} + +export interface GetSignedJWTTaskDef extends CommonTaskDef { + type: TaskType.GET_SIGNED_JWT; + inputParameters: { + subject: string; + issuer: string; + privateKey: string; + privateKeyId: string; + audience: string; + ttlInSecond: number; + scopes: string[]; + algorithm: GetSignedJWTAlgorithmType; + }; +} + +export interface QueryProcessorInputParameters { + workflowNames: string[]; + startTimeFrom?: number; + startTimeTo?: number; + correlationIds?: string[]; + freeText?: string; + statuses: string[]; + queryType: QueryProcessorType; +} +export interface QueryProcessorTaskDef extends CommonTaskDef { + type: TaskType.QUERY_PROCESSOR; + inputParameters: QueryProcessorInputParameters; +} + +export interface OpsGenieTaskDef extends CommonTaskDef { + type: TaskType.OPS_GENIE; + inputParameters: { + alias: string; + description: string; + visibleTo: { id: string; type: string }[]; + details?: { + [x: string]: string; + }; + message: string; + priority?: string; + responders: { type: string; username: string }[]; + actions?: string[]; + entity?: string; + token?: string; + tags?: string[]; + }; +} + +export interface LLMTextCompleteTaskDef extends CommonTaskDef { + type: TaskType.LLM_TEXT_COMPLETE; + inputParameters: { + llmProvider?: string; + model?: string; + promptName?: string; + promptVariables?: Record; + temperature?: number; + topP?: number; + maxTokens?: number; + }; +} + +export interface LLMGenerateEmbeddings extends CommonTaskDef { + type: TaskType.LLM_GENERATE_EMBEDDINGS; + inputParameters: { + llmProvider?: string; + model?: string; + text?: string; + }; +} + +export interface LLMGetEmbeddings extends CommonTaskDef { + type: TaskType.LLM_GET_EMBEDDINGS; + inputParameters: { + vectorDB?: string; + namespace?: string; + index?: string; + embedding?: number[]; + }; +} + +export interface LLMStoreEmbeddings extends CommonTaskDef { + type: TaskType.LLM_SEARCH_INDEX; + inputParameters: { + vectorDB?: string; + namespace?: string; + index?: string; + embeddingModelProvider?: string; + embeddingModel?: string; + id?: string; + }; +} + +export interface LLMIndexDocument extends CommonTaskDef { + type: TaskType.LLM_INDEX_DOCUMENT; + inputParameters: { + vectorDB?: string; + namespace?: string; + index?: string; + embeddingModelProvider?: string; + embeddingModel?: string; + url?: string; + mediaType?: string; + chunkSize?: number; + chunkOverlap?: number; + }; +} + +export interface LLMSearchIndex extends CommonTaskDef { + type: TaskType.LLM_SEARCH_INDEX; + inputParameters: { + vectorDB?: string; + namespace?: string; + index?: string; + llmProvider?: string; + }; +} + +export interface LLMIndexText extends CommonTaskDef { + type: TaskType.LLM_INDEX_TEXT; + inputParameters: { + vectorDB?: string; + namespace?: string; + index?: string; + embeddingModelProvider?: string; + embeddingModel?: string; + docId?: string; + text?: string; + }; +} + +export interface GetDocumentTaskDef extends CommonTaskDef { + type: TaskType.GET_DOCUMENT; + inputParameters: { + url?: string; + mediaType?: string; + }; +} + +export interface UpdateTaskDef extends CommonTaskDef { + type: TaskType.UPDATE_TASK; + inputParameters: { + taskStatus: string; + workflowId?: string; + taskRefName?: string; + taskId?: string; + mergeOutput: boolean; + taskOutput?: Record; + }; +} + +export interface GetWorkflowDef extends CommonTaskDef { + type: TaskType.GET_WORKFLOW; + inputParameters: { + id: string; + includeTasks: boolean; + }; +} + +export interface LLMChatComplete extends CommonTaskDef { + type: TaskType.LLM_CHAT_COMPLETE; + inputParameters: { + llmProvider: string; + model?: string; + instructions?: string; + messages?: string; + }; +} + +export interface ChunkTextTaskDef extends CommonTaskDef { + type: TaskType.CHUNK_TEXT; + inputParameters: { + text?: string; + chunkSize?: number; + mediaType?: string; + }; +} + +export interface ListFilesTaskDef extends CommonTaskDef { + type: TaskType.LIST_FILES; + inputParameters: { + inputLocation: string; + integrationName?: string; + outputLocation?: string; + fileTypes?: string[]; + integrationNames?: Record; + }; +} + +export interface ParseDocumentTaskDef extends CommonTaskDef { + type: TaskType.PARSE_DOCUMENT; + inputParameters: { + integrationName: string; + url?: string; + mediaType?: string; + chunkSize?: number; + }; +} + +export type LLMTaskTypes = + | LLMGenerateEmbeddings + | LLMGetEmbeddings + | LLMIndexDocument + | LLMSearchIndex + | LLMIndexText + | GetDocumentTaskDef + | LLMTextCompleteTaskDef + | LLMChatComplete; + +export type FormTaskType = + | TaskType.WAIT + | TaskType.HTTP + | TaskType.KAFKA_PUBLISH + | TaskType.HUMAN + | TaskType.BUSINESS_RULE + | TaskType.SENDGRID + | TaskType.WAIT_FOR_WEBHOOK + | TaskType.HTTP_POLL + | TaskType.DO_WHILE + | TaskType.SIMPLE + | TaskType.YIELD + | TaskType.JDBC + | TaskType.EVENT + | TaskType.JOIN + | TaskType.FORK_JOIN + | TaskType.FORK_JOIN_DYNAMIC + | TaskType.DYNAMIC + | TaskType.INLINE + | TaskType.SWITCH + | TaskType.JSON_JQ_TRANSFORM + | TaskType.TERMINATE + | TaskType.SET_VARIABLE + | TaskType.TERMINATE_WORKFLOW + | TaskType.SUB_WORKFLOW + | TaskType.START_WORKFLOW + | TaskType.LLM_TEXT_COMPLETE + | TaskType.LLM_GENERATE_EMBEDDINGS + | TaskType.LLM_GET_EMBEDDINGS + | TaskType.LLM_STORE_EMBEDDINGS + | TaskType.LLM_INDEX_DOCUMENT + | TaskType.LLM_SEARCH_INDEX + | TaskType.LLM_INDEX_TEXT + | TaskType.GET_DOCUMENT + | TaskType.UPDATE_SECRET + | TaskType.QUERY_PROCESSOR + | TaskType.OPS_GENIE + | TaskType.UPDATE_TASK + | TaskType.GET_WORKFLOW + | TaskType.LLM_CHAT_COMPLETE + | TaskType.GET_SIGNED_JWT + | TaskType.GRPC + | TaskType.MCP + | TaskType.CHUNK_TEXT + | TaskType.LIST_FILES + | TaskType.PARSE_DOCUMENT; diff --git a/ui-next/src/types/TestTaskTypes.ts b/ui-next/src/types/TestTaskTypes.ts new file mode 100644 index 0000000000..aa20bf4964 --- /dev/null +++ b/ui-next/src/types/TestTaskTypes.ts @@ -0,0 +1,67 @@ +import { WorkflowExecution, WorkflowExecutionStatus } from "./Execution"; +import { TaskDef } from "./common"; + +export interface OpenTestTaskButtonProps { + task: string | any; + maxHeight: number; + disabled?: boolean; + showForm?: boolean; + tasksList?: Partial[]; +} + +export interface TestTaskButtonProps { + task: string | any; + maxHeight: number; + onDismiss: () => void; + showForm: boolean; + tasksList?: Partial[]; +} + +export interface TestTaskProps { + taskModel: Record; + onChangeModel: (modelChanges: Record) => void; + domain: string; + onChangeDomain: (value: string) => void; + value: Record; + maxHeight: number; + handleRunTestTask: () => void; + isInProgress: boolean; + onDismiss: () => void; + testExecutionId?: string; + testedTaskExecutionResult: WorkflowExecution; + showForm: boolean; +} + +export interface TestControlsProps { + taskModel: Record; + value: Record; + isInProgress: boolean; + handleRunTestTask: () => void; + onChangeModel: (modelChanges: Record) => void; + domain: string; + onChangeDomain: (value: string) => void; + showForm: boolean; +} + +export interface TestOutputProps { + onChangeModel: (modelChanges: Record) => void; + testedTaskExecutionResult: WorkflowExecution; + status: WorkflowExecutionStatus; + testExecutionId?: string; +} + +export interface FormSectionProps { + extractedJsonVariables: Record; + onChangeModel: (modelChanges: Record) => void; + value: Record; + domain: string; + onChangeDomain: (value: string) => void; +} + +export interface JsonSectionProps { + handleJSONChange: (newValue: string) => void; + taskModel: Record; + value: Record; + domain: string; + onChangeDomain: (value: string) => void; +} diff --git a/ui-next/src/types/TimeoutPolicy.ts b/ui-next/src/types/TimeoutPolicy.ts new file mode 100644 index 0000000000..a81a7fa96e --- /dev/null +++ b/ui-next/src/types/TimeoutPolicy.ts @@ -0,0 +1,4 @@ +export enum TimeoutPolicy { + TIME_OUT_WF = "TIME_OUT_WF", + ALERT_ONLY = "ALERT_ONLY", +} diff --git a/ui-next/src/types/UpdateTaskStatus.ts b/ui-next/src/types/UpdateTaskStatus.ts new file mode 100644 index 0000000000..991a36479b --- /dev/null +++ b/ui-next/src/types/UpdateTaskStatus.ts @@ -0,0 +1,5 @@ +export enum UpdateTaskStatus { + COMPLETED = "COMPLETED", + FAILED = "FAILED", + FAILED_WITH_TERMINAL_ERROR = "FAILED_WITH_TERMINAL_ERROR", +} diff --git a/ui-next/src/types/User.ts b/ui-next/src/types/User.ts new file mode 100644 index 0000000000..47f58c6ac1 --- /dev/null +++ b/ui-next/src/types/User.ts @@ -0,0 +1,55 @@ +export interface Auth0User { + given_name?: string; + family_name?: string; + nickname?: string; + name?: string; + picture?: string; + locale?: string; + updated_at?: string; + email?: string; + email_verified?: boolean; + sub?: string; +} +export interface OktaUser { + sub: string; + name: string; + locale: string; + email: string; + preferred_username: string; + given_name: string; + family_name: string; + zoneinfo: string; + updated_at: number; + email_verified: boolean; +} + +export interface AccessPermission { + name: string; +} + +export interface AccessRole { + name: string; + permissions?: AccessPermission[]; +} + +export interface AccessGroup { + id: string; + description: string; + roles: AccessRole[]; + defaultAccess: unknown; // TODO fixme +} + +export interface User { + applicationUser: boolean; + groups: AccessGroup[]; + id: string; + name: string; + roles: AccessRole[]; + uuid: string; +} + +export interface UserContext { + user: Partial; + accessToken?: string; + isAuthenticated: boolean; +} diff --git a/ui-next/src/types/WebhookDefinition.ts b/ui-next/src/types/WebhookDefinition.ts new file mode 100644 index 0000000000..9c12f16da4 --- /dev/null +++ b/ui-next/src/types/WebhookDefinition.ts @@ -0,0 +1,23 @@ +export interface WebhookExecution { + eventId: string; + matched: boolean; + workflowIds: string[]; + payload: string; + timeStamp: number; +} + +export interface WebhookDefinition { + name: string; + receiverWorkflowNamesToVersions: Record; + sourcePlatform: string; + id?: string; + workflowsToStart: Record; + headers: Record; + webhookExecutionHistory: WebhookExecution[]; + urlVerified: boolean; + verifier: string; + headerKey: string; + secretKey: string; + secretValue: string; + createdBy: string; +} diff --git a/ui-next/src/types/WorkflowDef.ts b/ui-next/src/types/WorkflowDef.ts new file mode 100644 index 0000000000..b95098ab78 --- /dev/null +++ b/ui-next/src/types/WorkflowDef.ts @@ -0,0 +1,36 @@ +import { TaskDef } from "./common"; +import { TagDto } from "./Tag"; + +export enum TimeoutPolicy { + RETRY = "RETRY", + TIME_OUT_WF = "TIME_OUT_WF", + ALERT_ONLY = "ALERT_ONLY", +} + +export interface WorkflowMetadataI { + name: string; + description: string; + version: number; + inputParameters?: string[]; + outputParameters?: Record; + restartable: boolean; + timeoutSeconds: number; + timeoutPolicy?: TimeoutPolicy; + failureWorkflow?: string; + ownerEmail: string; + updateTime: number; + workflowStatusListenerEnabled: boolean; + createTime?: number; + workflowStatusListenerSink?: string; + metadata?: Record; + inputSchema?: Record; + outputSchema?: Record; + enforceSchema?: boolean; +} +export type WorkflowDef = { + failureWorkflow: string; + schemaVersion: number; + tasks: TaskDef[]; + tags?: TagDto[]; + inputSchema?: Record; +} & WorkflowMetadataI; diff --git a/ui-next/src/types/WorkflowExecution.ts b/ui-next/src/types/WorkflowExecution.ts new file mode 100644 index 0000000000..a538f58642 --- /dev/null +++ b/ui-next/src/types/WorkflowExecution.ts @@ -0,0 +1,19 @@ +import { QueryDispatch, SetStateAction } from "react-router-use-location-state"; +import { TaskExecutionResult } from "./TaskExecution"; + +export type QueryFTType = { + query: string; + freeText: string; +}; + +export type ResultObjType = TaskExecutionResult; + +export interface DoSearchProps { + resultObj: ResultObjType; + queryFT: QueryFTType; + buildQuery: (defaultStartTime?: string) => QueryFTType; + setQueryFT: (value: QueryFTType) => void; + refetch: () => void; + setPage: QueryDispatch>; + setRecentTaskSearch?: () => void; +} diff --git a/ui-next/src/types/common.ts b/ui-next/src/types/common.ts new file mode 100644 index 0000000000..120aa2475c --- /dev/null +++ b/ui-next/src/types/common.ts @@ -0,0 +1,173 @@ +import { AlertColor } from "@mui/material"; +import { TaskDefinitionDto } from "./TaskDefinition"; + +export interface IObject { + [key: string]: any; +} + +export type AuthHeaders = Record; + +export type HasAuthHeaders = { + authHeaders: AuthHeaders; +}; + +export const FIELD_TYPE_STRING = "string"; +export const FIELD_TYPE_NUMBER = "number"; +export const FIELD_TYPE_OBJECT = "object"; +export const FIELD_TYPE_BOOLEAN = "boolean"; +export const FIELD_TYPE_NULL = "null"; + +export type FieldType = + | typeof FIELD_TYPE_STRING + | typeof FIELD_TYPE_NUMBER + | typeof FIELD_TYPE_OBJECT + | typeof FIELD_TYPE_BOOLEAN + | typeof FIELD_TYPE_NULL; + +export type CoerceToType = "integer" | "double" | "string"; + +export interface Tag { + key: string; + value: string; + type: "METADATA" | "RATE_LIMIT"; +} + +export enum TaskType { + START = "START", + SIMPLE = "SIMPLE", + YIELD = "YIELD", + DYNAMIC = "DYNAMIC", + FORK_JOIN = "FORK_JOIN", + FORK_JOIN_DYNAMIC = "FORK_JOIN_DYNAMIC", + DECISION = "DECISION", + SWITCH = "SWITCH", + JOIN = "JOIN", + DO_WHILE = "DO_WHILE", + SUB_WORKFLOW = "SUB_WORKFLOW", + EVENT = "EVENT", + WAIT = "WAIT", + USER_DEFINED = "USER_DEFINED", + HTTP = "HTTP", + LAMBDA = "LAMBDA", + INLINE = "INLINE", + EXCLUSIVE_JOIN = "EXCLUSIVE_JOIN", + TERMINAL = "TERMINAL", + TERMINATE = "TERMINATE", + KAFKA_PUBLISH = "KAFKA_PUBLISH", + JSON_JQ_TRANSFORM = "JSON_JQ_TRANSFORM", + SET_VARIABLE = "SET_VARIABLE", + TERMINATE_WORKFLOW = "TERMINATE_WORKFLOW", + HUMAN = "HUMAN", + WAIT_FOR_EVENT = "WAIT_FOR_EVENT", + TASK_SUMMARY = "TASK_SUMMARY", + BUSINESS_RULE = "BUSINESS_RULE", + SENDGRID = "SENDGRID", + WAIT_FOR_WEBHOOK = "WAIT_FOR_WEBHOOK", + START_WORKFLOW = "START_WORKFLOW", + HTTP_POLL = "HTTP_POLL", + JDBC = "JDBC", + SWITCH_JOIN = "SWITCH_JOIN", // Pseudo task. doesn't really exist on the workflow + IA_TASK = "_ai_tc", + LLM_TEXT_COMPLETE = "LLM_TEXT_COMPLETE", + LLM_GENERATE_EMBEDDINGS = "LLM_GENERATE_EMBEDDINGS", + LLM_GET_EMBEDDINGS = "LLM_GET_EMBEDDINGS", + LLM_STORE_EMBEDDINGS = "LLM_STORE_EMBEDDINGS", + LLM_SEARCH_INDEX = "LLM_SEARCH_INDEX", + LLM_INDEX_DOCUMENT = "LLM_INDEX_DOCUMENT", + GET_DOCUMENT = "GET_DOCUMENT", + LLM_INDEX_TEXT = "LLM_INDEX_TEXT", + UPDATE_SECRET = "UPDATE_SECRET", + JUMP = "JUMP", + QUERY_PROCESSOR = "QUERY_PROCESSOR", + OPS_GENIE = "OPS_GENIE", + GET_SIGNED_JWT = "GET_SIGNED_JWT", + UPDATE_TASK = "UPDATE_TASK", + GET_WORKFLOW = "GET_WORKFLOW", + LLM_CHAT_COMPLETE = "LLM_CHAT_COMPLETE", + GRPC = "GRPC", + MCP = "MCP", + MCP_REMOTE = "MCP_REMOTE", + CHUNK_TEXT = "CHUNK_TEXT", + LIST_FILES = "LIST_FILES", + PARSE_DOCUMENT = "PARSE_DOCUMENT", +} + +export interface TaskDef { + name: string; + taskReferenceName: string; + description: string; + inputParameters?: IObject; + decisionCases?: Record; + type: TaskType; + dynamicTaskNameParam?: string; + caseValueParam?: string; + caseExpression?: string; + scriptExpression?: string; + dynamicForkTasksParam?: string; + dynamicForkTasksInputParamName?: string; + defaultCase?: TaskDef[]; + forkTasks?: Array; + startDelay: number; + joinOn: string[]; + sink?: string; + evaluatorType?: string; + expression?: string; + loopConditionType?: string; + loopCondition?: string; + optional: boolean; + defaultExclusiveJoinTask: string[]; + loopOver?: TaskDef[]; + subWorkflowParam?: { + name?: string; + version?: number | string; + workflowDefinition?: string | object; + idempotencyKey?: string; + idempotencyStrategy?: string; + priority?: string | number; + }; + asyncComplete?: boolean; + triggerFailureWorkflow?: boolean; + taskStatus?: string; + workflowId?: string; + taskRefName?: string; + taskId?: string; + mergeOutput?: boolean; + taskOutput?: Record; + iteration?: number; + taskDefinition?: TaskDefinitionDto; +} + +export interface TaskDto extends TaskDef { + executable?: boolean; + tags?: Tag[]; + createTime?: number; + ownerEmail?: string; + inputKeys?: string[]; + outputKeys?: string[]; + timeoutPolicy?: string; + timeoutSeconds?: number; + retryCount?: number; + retryLogic?: boolean; + retryDelaySeconds?: number; + responseTimeoutSeconds?: number; + inputTemplate?: string; + rateLimitPerFrequency?: string; + rateLimitFrequencyInSeconds?: number; +} + +export interface ErrorObj { + message?: string; + severity?: AlertColor; +} + +export type TryFn = () => Promise; + +export type NullifyValues = { + [K in keyof T]: T[K] extends object + ? T[K] extends null | undefined + ? null + : NullifyValues | null + : T[K] extends undefined + ? undefined + : T[K] | null; +}; diff --git a/ui-next/src/types/helperTypes.ts b/ui-next/src/types/helperTypes.ts new file mode 100644 index 0000000000..9433cc45d0 --- /dev/null +++ b/ui-next/src/types/helperTypes.ts @@ -0,0 +1,3 @@ +export type Entries = { + [K in keyof T]: [K, T[K]]; +}[keyof T][]; diff --git a/ui-next/src/types/index.ts b/ui-next/src/types/index.ts new file mode 100644 index 0000000000..6958029df0 --- /dev/null +++ b/ui-next/src/types/index.ts @@ -0,0 +1,23 @@ +export * from "./TaskStatus"; +export * from "./TaskType"; +export * from "./Schemas"; +export * from "./SchemasAjv"; +export * from "./Execution"; +export * from "./WorkflowDef"; +export * from "./common"; +export * from "./User"; +export * from "./Environment"; +export * from "./helperTypes"; +export * from "./Crumbs"; +export * from "./HumanTaskTypes"; +export * from "./Events"; +export * from "./Application"; +export * from "./TaskLog"; +export * from "./FormFieldTypes"; +export * from "./Tag"; +export * from "./Integrations"; +export * from "./Messages"; +export * from "./TaskDefinition"; +export * from "./Prompts"; +export * from "./TaskExecution"; +export * from "./EnvVariables"; diff --git a/ui-next/src/types/svg.d.ts b/ui-next/src/types/svg.d.ts new file mode 100644 index 0000000000..7528ad7bcb --- /dev/null +++ b/ui-next/src/types/svg.d.ts @@ -0,0 +1,7 @@ +/// + +declare module "*.svg?react" { + import * as React from "react"; + const ReactComponent: React.FunctionComponent>; + export default ReactComponent; +} diff --git a/ui-next/src/useArrowNavigation.tsx b/ui-next/src/useArrowNavigation.tsx new file mode 100644 index 0000000000..9095a25323 --- /dev/null +++ b/ui-next/src/useArrowNavigation.tsx @@ -0,0 +1,192 @@ +import { + useCallback, + useMemo, + useState, + KeyboardEvent, + MouseEvent, +} from "react"; +import _nth from "lodash/fp/nth"; +import _first from "lodash/fp/first"; +import _last from "lodash/fp/last"; + +type useArrowNavigationProps = { + onSelect: (item: T) => void; + options: T[]; + optionsIdGen: (v: T) => string; + scrollToCenter: boolean; + hoveredItem: string; + setHoveredItem: (item: string) => void; +}; + +export type OptionPropsForItemT = { + onMouseMove: (e: any) => void; + onMouseLeave: (e: any) => void; + id: string; +}; + +function useArrowNavigation({ + onSelect, + options, + optionsIdGen, + scrollToCenter, + hoveredItem, + setHoveredItem, +}: useArrowNavigationProps) { + const [lastCursorPos, setLastCursorPos] = useState({ x: 0, y: 0 }); + + const [firstOptionItemHash, lastOptionItemHash] = useMemo(() => { + const head = _first(options); + const tail = _last(options); + + return [ + head ? optionsIdGen(head) : undefined, + tail ? optionsIdGen(tail) : undefined, + ]; + }, [options, optionsIdGen]); + + const [hoveredOptionValue, hoveredOptionValueIndex] = useMemo(() => { + const idx = options.findIndex( + (item: T) => hoveredItem === optionsIdGen(item), + ); + if (idx === -1) { + return [undefined, -1]; + } + return [_nth(idx, options), idx]; + }, [hoveredItem, options, optionsIdGen]); + + const moveDown = useCallback(() => { + if (hoveredItem !== "") { + if (options && options.length > 0) { + //get the index of hoveredItem from options and then add +1 + const nextIndex = hoveredOptionValueIndex + 1; + const maybeNextItem = _nth( + nextIndex < options.length ? nextIndex : 0, + options, + ); + + if (maybeNextItem) { + const nextElementHash = optionsIdGen(maybeNextItem); + setHoveredItem(nextElementHash); + if (typeof window !== "undefined") { + window.document.getElementById(nextElementHash)?.scrollIntoView({ + behavior: "smooth", + block: scrollToCenter ? "center" : "nearest", + inline: scrollToCenter ? "center" : "start", + }); + } + } + } + } else if (firstOptionItemHash) { + setHoveredItem(firstOptionItemHash); + } + }, [ + firstOptionItemHash, + hoveredItem, + hoveredOptionValueIndex, + options, + optionsIdGen, + scrollToCenter, + setHoveredItem, + ]); + + const moveUp = useCallback(() => { + if (hoveredItem !== "") { + if (options && options.length > 0) { + //get the index of hoveredItem from options and then add -1 + const maybePreviousItem = _nth(hoveredOptionValueIndex - 1, options); + + if (maybePreviousItem) { + const previousElementHash = optionsIdGen(maybePreviousItem); + setHoveredItem(previousElementHash); + if (typeof window !== "undefined") { + window.document + .getElementById(previousElementHash) + ?.scrollIntoView({ + behavior: "smooth", + block: scrollToCenter ? "center" : "nearest", + inline: scrollToCenter ? "center" : "start", + }); + } + } + } + } else if (lastOptionItemHash) { + setHoveredItem(lastOptionItemHash); + } + }, [ + lastOptionItemHash, + hoveredItem, + hoveredOptionValueIndex, + options, + optionsIdGen, + scrollToCenter, + setHoveredItem, + ]); + + const handleKeyDown = useCallback( + (event: KeyboardEvent) => { + if (event.key === "Enter") { + if (options && hoveredItem) { + if (hoveredOptionValue) { + onSelect(hoveredOptionValue); + } + } + } + if (event.key === "ArrowDown") { + event.preventDefault(); + moveDown(); + } + if (event.key === "ArrowUp") { + event.preventDefault(); + moveUp(); + } + }, + [options, hoveredItem, hoveredOptionValue, moveDown, moveUp, onSelect], + ); + + const handleMouseLeave = useCallback((event: MouseEvent) => { + setLastCursorPos({ x: event.screenX, y: event.screenY }); + }, []); + const handleMouseOver = (e: MouseEvent, index: string) => { + const currentCursorPos = { + x: e.screenX, + y: e.screenY, + }; + + if ( + currentCursorPos.x === lastCursorPos.x && + currentCursorPos.y === lastCursorPos.y + ) { + return; + } + setLastCursorPos({ x: e.screenX, y: e.screenY }); + + setHoveredItem(index); + }; + + const optionPropsForItem = (item: T): OptionPropsForItemT => { + return { + onMouseMove: (e: any) => { + handleMouseOver(e, optionsIdGen(item)); + }, + onMouseLeave: (e: any) => { + handleMouseLeave(e); + }, + id: optionsIdGen(item), + }; + }; + + const inputProps = { + onKeyDown: handleKeyDown, + }; + + return { + inputProps, + optionPropsForItem, + hoveredItem, + moveUp, + moveDown, + } as const; +} + +export default useArrowNavigation; +export type { useArrowNavigationProps }; diff --git a/ui-next/src/utils/__tests__/date.test.ts b/ui-next/src/utils/__tests__/date.test.ts new file mode 100644 index 0000000000..cb9b3ad75b --- /dev/null +++ b/ui-next/src/utils/__tests__/date.test.ts @@ -0,0 +1,51 @@ +import { printableUpdatedTime } from "utils/date"; + +describe("printableUpdatedTime", () => { + afterEach(() => { + vi.useRealTimers(); // restore real timers after each test + }); + + it('should return "0" if updatedTimeInMillis is null', () => { + const result = printableUpdatedTime(null as unknown as number); + expect(result).toBe("0 minutes ago"); + }); + + it('should return "0" if updatedTimeInMillis is 0', () => { + const result = printableUpdatedTime(0); + expect(result).toBe("0 minutes ago"); + }); + + it("should handle time difference in minutes", () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date(2024, 9, 1, 12, 0, 0)); // Set a fixed date + const result = printableUpdatedTime(new Date(2024, 9, 1, 11, 58).getTime()); // 2 minutes ago + expect(result).toBe("2 minutes ago"); + }); + + it("should handle time difference in days", () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date(2024, 9, 10, 12, 0, 0)); // Set a fixed date + const result = printableUpdatedTime( + new Date(2024, 9, 5, 12, 0, 0).getTime(), + ); // 5 days ago + expect(result).toBe("5 days ago"); + }); + + it("should handle time difference in months", () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date(2024, 9, 1, 12, 0, 0)); // Set a fixed date + const result = printableUpdatedTime( + new Date(2024, 6, 1, 12, 0, 0).getTime(), + ); // 3 months ago + expect(result).toBe("3 months ago"); + }); + + it("should handle time difference in years", () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date(2024, 9, 1, 12, 0, 0)); // Set a fixed date + const result = printableUpdatedTime( + new Date(2022, 9, 1, 12, 0, 0).getTime(), + ); // 2 years ago + expect(result).toBe("about 2 years ago"); + }); +}); diff --git a/ui-next/src/utils/__tests__/json.test.ts b/ui-next/src/utils/__tests__/json.test.ts new file mode 100644 index 0000000000..f14e541904 --- /dev/null +++ b/ui-next/src/utils/__tests__/json.test.ts @@ -0,0 +1,1098 @@ +import { extractVariablesFromJSON, downgradeSchemaToDraft7 } from "utils/json"; +import { isJSONSchemaValid } from "utils/jsonSchema"; + +const json = { + uri: "${workflow.input.pepe}", + method: "GET", + connectionTimeOut: 3000, + readTimeOut: "3000", + accept: "application/json", + contentType: "application/json", +}; + +const emptyJson = { + uri: "https://orkes-api-tester.orkesconductor.com/api", + method: "GET", + connectionTimeOut: 3000, + readTimeOut: "3000", + accept: "application/json", + contentType: "application/json", +}; + +const nestedJson = { + uri: "${workflow.input.pepe}", + method: "GET", + headers: { + accept: "${workflow.input.pepe1}", + }, + connectionTimeOut: 3000, + readTimeOut: "3000", + accept: "application/json", + contentType: "application/json", +}; + +describe("Extract json variables", () => { + it("should return all variables from json", () => { + const expected = { uri: "workflow.input.pepe" }; + expect(extractVariablesFromJSON(json)).toEqual(expected); + }); + + it("should return empty object if no variables present in the json", () => { + expect(extractVariablesFromJSON(emptyJson)).toEqual({}); + }); + + it("should return all variables from json with headers.accept", () => { + const expected = { + uri: "workflow.input.pepe", + "headers.accept": "workflow.input.pepe1", + }; + expect(extractVariablesFromJSON(nestedJson)).toEqual(expected); + }); +}); + +describe("downgradeSchemaToDraft7", () => { + describe("input validation", () => { + it("should return empty object for null input", () => { + expect(downgradeSchemaToDraft7(null as any)).toEqual({}); + }); + + it("should return empty object for undefined input", () => { + expect(downgradeSchemaToDraft7(undefined as any)).toEqual({}); + }); + + it("should return empty object for non-object input", () => { + expect(downgradeSchemaToDraft7("string" as any)).toEqual({}); + expect(downgradeSchemaToDraft7(123 as any)).toEqual({}); + expect(downgradeSchemaToDraft7(true as any)).toEqual({}); + }); + + it("should return array as-is for array input", () => { + const array = [{ type: "string" }]; + expect(downgradeSchemaToDraft7(array as any)).toEqual(array); + }); + }); + + describe("schema version conversion", () => { + it("should convert Draft 2019-09 schema to Draft 7", () => { + const schema = { + $schema: "https://json-schema.org/draft/2019-09/schema", + type: "object", + properties: { + name: { type: "string" }, + }, + }; + + const result = downgradeSchemaToDraft7(schema); + expect(result.$schema).toBe("http://json-schema.org/draft-07/schema#"); + expect(result.type).toBe("object"); + expect(result.properties).toEqual(schema.properties); + }); + + it("should convert Draft 2020-12 schema to Draft 7", () => { + const schema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "string", + }; + + const result = downgradeSchemaToDraft7(schema); + expect(result.$schema).toBe("http://json-schema.org/draft-07/schema#"); + expect(result.type).toBe("string"); + }); + + it("should return Draft 7 schema as-is", () => { + const schema = { + $schema: "http://json-schema.org/draft-07/schema#", + type: "object", + properties: { + name: { type: "string" }, + }, + }; + + const result = downgradeSchemaToDraft7(schema); + expect(result).toEqual(schema); + // Function returns original schema when no changes are needed (performance optimization) + expect(result).toBe(schema); + }); + + it("should convert Draft 6 schema to Draft 7", () => { + const schema = { + $schema: "http://json-schema.org/draft-06/schema#", + type: "string", + }; + + const result = downgradeSchemaToDraft7(schema); + expect(result.$schema).toBe("http://json-schema.org/draft-07/schema#"); + expect(result.type).toBe("string"); + // Should be a copy, not the original + expect(result).not.toBe(schema); + }); + + it("should convert Draft 4 schema to Draft 7", () => { + const schema = { + $schema: "http://json-schema.org/draft-04/schema#", + type: "object", + properties: { + name: { type: "string" }, + }, + }; + + const result = downgradeSchemaToDraft7(schema); + expect(result.$schema).toBe("http://json-schema.org/draft-07/schema#"); + expect(result.type).toBe("object"); + expect(result.properties).toEqual(schema.properties); + // Should be a copy, not the original + expect(result).not.toBe(schema); + }); + + it("should handle schema without $schema field but with newer keywords", () => { + const schema = { + type: "object", + properties: { + name: { type: "string" }, + }, + $defs: { + address: { type: "object" }, + }, + }; + + const result = downgradeSchemaToDraft7(schema); + expect(result.$schema).toBe("http://json-schema.org/draft-07/schema#"); + expect(result.type).toBe("object"); + expect(result.definitions).toBeDefined(); + }); + + it("should add $schema to Draft 7 when missing, even with no newer keywords", () => { + const schema = { + type: "object", + properties: { + name: { type: "string" }, + }, + }; + + const result = downgradeSchemaToDraft7(schema); + // Should add $schema for JsonForms compatibility even if no newer keywords + expect(result.$schema).toBe("http://json-schema.org/draft-07/schema#"); + expect(result.type).toBe("object"); + expect(result.properties).toEqual(schema.properties); + // Should be a copy, not the original + expect(result).not.toBe(schema); + }); + }); + + describe("$defs to definitions conversion", () => { + it("should convert $defs to definitions", () => { + const schema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + $defs: { + address: { + type: "object", + properties: { + street: { type: "string" }, + }, + }, + }, + }; + + const result = downgradeSchemaToDraft7(schema); + expect(result.$defs).toBeUndefined(); + expect(result.definitions).toBeDefined(); + expect(result.definitions.address).toEqual(schema.$defs.address); + }); + + it("should merge $defs into existing definitions", () => { + const schema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + definitions: { + existing: { type: "string" }, + }, + $defs: { + newDef: { type: "number" }, + }, + }; + + const result = downgradeSchemaToDraft7(schema); + expect(result.definitions.existing).toBeDefined(); + expect(result.definitions.newDef).toBeDefined(); + expect(result.$defs).toBeUndefined(); + }); + + it("should process nested $defs recursively", () => { + const schema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + $defs: { + address: { + type: "object", + $defs: { + nested: { type: "string" }, + }, + }, + }, + }; + + const result = downgradeSchemaToDraft7(schema); + expect(result.definitions.address.definitions).toBeDefined(); + expect(result.definitions.address.definitions.nested).toBeDefined(); + expect(result.definitions.address.$defs).toBeUndefined(); + }); + }); + + describe("$ref updates", () => { + it("should update $ref from $defs to definitions", () => { + const schema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + address: { $ref: "#/$defs/address" }, + }, + $defs: { + address: { type: "object" }, + }, + }; + + const result = downgradeSchemaToDraft7(schema); + expect(result.properties.address.$ref).toBe("#/definitions/address"); + expect(result.definitions.address).toBeDefined(); + }); + + it("should not modify $ref that doesn't reference $defs", () => { + const schema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + address: { $ref: "#/definitions/address" }, + }, + definitions: { + address: { type: "object" }, + }, + }; + + const result = downgradeSchemaToDraft7(schema); + expect(result.properties.address.$ref).toBe("#/definitions/address"); + }); + }); + + describe("unsupported keywords removal", () => { + it("should remove unevaluatedProperties", () => { + const schema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + unevaluatedProperties: false, + properties: { + name: { type: "string" }, + }, + }; + + const result = downgradeSchemaToDraft7(schema); + expect(result.unevaluatedProperties).toBeUndefined(); + expect(result.properties).toBeDefined(); + }); + + it("should remove unevaluatedItems", () => { + const schema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "array", + unevaluatedItems: false, + items: { type: "string" }, + }; + + const result = downgradeSchemaToDraft7(schema); + expect(result.unevaluatedItems).toBeUndefined(); + expect(result.items).toBeDefined(); + }); + + it("should remove dependentRequired", () => { + const schema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + dependentRequired: { + credit_card: ["billing_address"], + }, + }; + + const result = downgradeSchemaToDraft7(schema); + expect(result.dependentRequired).toBeUndefined(); + }); + + it("should remove dependentSchemas", () => { + const schema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + dependentSchemas: { + credit_card: { type: "object" }, + }, + }; + + const result = downgradeSchemaToDraft7(schema); + expect(result.dependentSchemas).toBeUndefined(); + }); + + it("should remove $anchor, $dynamicAnchor, and $dynamicRef", () => { + const schema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + $anchor: "myAnchor", + $dynamicAnchor: "myDynamicAnchor", + $dynamicRef: "#myDynamicAnchor", + }; + + const result = downgradeSchemaToDraft7(schema); + expect(result.$anchor).toBeUndefined(); + expect(result.$dynamicAnchor).toBeUndefined(); + expect(result.$dynamicRef).toBeUndefined(); + }); + + it("should remove minContains and maxContains", () => { + const schema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "array", + contains: { type: "string" }, + minContains: 2, + maxContains: 5, + }; + + const result = downgradeSchemaToDraft7(schema); + expect(result.minContains).toBeUndefined(); + expect(result.maxContains).toBeUndefined(); + expect(result.contains).toBeDefined(); + }); + }); + + describe("nested schema processing", () => { + it("should process nested properties", () => { + const schema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + user: { + type: "object", + properties: { + name: { type: "string" }, + address: { $ref: "#/$defs/address" }, + }, + }, + }, + $defs: { + address: { type: "object" }, + }, + }; + + const result = downgradeSchemaToDraft7(schema); + expect(result.properties.user.properties.address.$ref).toBe( + "#/definitions/address", + ); + expect(result.definitions.address).toBeDefined(); + }); + + it("should process items (single schema)", () => { + const schema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "array", + items: { + type: "object", + $defs: { + nested: { type: "string" }, + }, + }, + }; + + const result = downgradeSchemaToDraft7(schema); + expect(result.items.definitions).toBeDefined(); + expect(result.items.$defs).toBeUndefined(); + }); + + it("should process items (array of schemas)", () => { + const schema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "array", + items: [ + { + type: "object", + $defs: { nested: { type: "string" } }, + }, + { type: "string" }, + ], + }; + + const result = downgradeSchemaToDraft7(schema); + expect(result.items[0].definitions).toBeDefined(); + expect(result.items[0].$defs).toBeUndefined(); + expect(result.items[1].type).toBe("string"); + }); + + it("should process allOf", () => { + const schema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + allOf: [ + { + type: "object", + $defs: { nested: { type: "string" } }, + }, + { type: "object", properties: { name: { type: "string" } } }, + ], + }; + + const result = downgradeSchemaToDraft7(schema); + expect(result.allOf[0].definitions).toBeDefined(); + expect(result.allOf[0].$defs).toBeUndefined(); + expect(result.allOf[1].properties).toBeDefined(); + }); + + it("should process anyOf", () => { + const schema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + anyOf: [ + { type: "string" }, + { + type: "object", + unevaluatedProperties: false, + }, + ], + }; + + const result = downgradeSchemaToDraft7(schema); + expect(result.anyOf[0].type).toBe("string"); + expect(result.anyOf[1].unevaluatedProperties).toBeUndefined(); + }); + + it("should process oneOf", () => { + const schema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + oneOf: [{ type: "string" }, { type: "number" }], + }; + + const result = downgradeSchemaToDraft7(schema); + expect(result.oneOf).toHaveLength(2); + expect(result.oneOf[0].type).toBe("string"); + expect(result.oneOf[1].type).toBe("number"); + }); + + it("should process not", () => { + const schema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + not: { + type: "object", + $defs: { nested: { type: "string" } }, + }, + }; + + const result = downgradeSchemaToDraft7(schema); + expect(result.not.definitions).toBeDefined(); + expect(result.not.$defs).toBeUndefined(); + }); + + it("should process if/then/else", () => { + const schema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + if: { + type: "object", + properties: { type: { const: "user" } }, + }, + then: { + type: "object", + $defs: { userSchema: { type: "object" } }, + }, + else: { + type: "object", + unevaluatedProperties: false, + }, + }; + + const result = downgradeSchemaToDraft7(schema); + expect(result.then.definitions).toBeDefined(); + expect(result.then.$defs).toBeUndefined(); + expect(result.else.unevaluatedProperties).toBeUndefined(); + }); + + it("should process patternProperties", () => { + const schema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + patternProperties: { + "^S_": { + type: "string", + $defs: { nested: { type: "string" } }, + }, + "^I_": { type: "integer" }, + }, + }; + + const result = downgradeSchemaToDraft7(schema); + expect(result.patternProperties["^S_"].definitions).toBeDefined(); + expect(result.patternProperties["^I_"].type).toBe("integer"); + }); + + it("should process additionalProperties (schema object)", () => { + const schema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + additionalProperties: { + type: "string", + $defs: { nested: { type: "string" } }, + }, + }; + + const result = downgradeSchemaToDraft7(schema); + expect(result.additionalProperties.definitions).toBeDefined(); + }); + }); + + describe("complex nested scenarios", () => { + it("should handle deeply nested schemas", () => { + const schema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + user: { + type: "object", + properties: { + profile: { + type: "object", + allOf: [ + { + type: "object", + $defs: { + deep: { + type: "object", + properties: { + nested: { + $ref: "#/$defs/deep", + unevaluatedProperties: false, + }, + }, + }, + }, + }, + ], + }, + }, + }, + }, + $defs: { + topLevel: { type: "string" }, + }, + }; + + const result = downgradeSchemaToDraft7(schema); + expect(result.definitions.topLevel).toBeDefined(); + expect( + result.properties.user.properties.profile.allOf[0].definitions, + ).toBeDefined(); + expect( + result.properties.user.properties.profile.allOf[0].definitions.deep + .properties.nested.unevaluatedProperties, + ).toBeUndefined(); + }); + + it("should handle schema with all major features", () => { + const schema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + name: { type: "string" }, + items: { + type: "array", + items: { $ref: "#/$defs/item" }, + unevaluatedItems: false, + }, + }, + allOf: [ + { type: "object" }, + { + anyOf: [{ type: "object", properties: { x: { type: "number" } } }], + }, + ], + $defs: { + item: { + type: "object", + properties: { + value: { type: "string" }, + }, + unevaluatedProperties: false, + }, + }, + unevaluatedProperties: false, + dependentRequired: { x: ["y"] }, + minContains: 1, + }; + + const result = downgradeSchemaToDraft7(schema); + expect(result.$schema).toBe("http://json-schema.org/draft-07/schema#"); + expect(result.definitions.item).toBeDefined(); + expect(result.$defs).toBeUndefined(); + expect(result.properties.items.items.$ref).toBe("#/definitions/item"); + expect(result.unevaluatedProperties).toBeUndefined(); + expect(result.unevaluatedItems).toBeUndefined(); + expect(result.dependentRequired).toBeUndefined(); + expect(result.minContains).toBeUndefined(); + expect(result.definitions.item.unevaluatedProperties).toBeUndefined(); + }); + }); + + describe("edge cases", () => { + it("should handle empty objects", () => { + const schema = {}; + const result = downgradeSchemaToDraft7(schema); + // Empty object with no newer keywords should be returned as-is + expect(result).toEqual({ + $schema: "http://json-schema.org/draft-07/schema#", + }); + expect(result.$schema).toBe("http://json-schema.org/draft-07/schema#"); + }); + + it("should handle empty objects with newer keywords", () => { + const schema = { + $defs: { + test: { type: "string" }, + }, + }; + const result = downgradeSchemaToDraft7(schema); + expect(result.$schema).toBe("http://json-schema.org/draft-07/schema#"); + expect(result.definitions).toBeDefined(); + }); + + it("should handle empty arrays in nested keys", () => { + const schema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + allOf: [], + anyOf: [], + oneOf: [], + items: [], + }; + + const result = downgradeSchemaToDraft7(schema); + expect(result.allOf).toEqual([]); + expect(result.anyOf).toEqual([]); + expect(result.oneOf).toEqual([]); + expect(result.items).toEqual([]); + }); + + it("should handle null values in nested keys", () => { + const schema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + properties: null, + items: null, + }; + + const result = downgradeSchemaToDraft7(schema); + expect(result.properties).toBeNull(); + expect(result.items).toBeNull(); + }); + + it("should not mutate original schema", () => { + const schema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + $defs: { + test: { type: "string" }, + }, + unevaluatedProperties: false, + }; + + const originalSchema = JSON.parse(JSON.stringify(schema)); + downgradeSchemaToDraft7(schema); + + // Original should be unchanged + expect(schema.$defs).toBeDefined(); + expect(schema.unevaluatedProperties).toBe(false); + expect(schema).toEqual(originalSchema); + }); + }); + + describe("real-world example schemas", () => { + it("should downgrade schema with teamId and first properties", () => { + const schema = { + additionalProperties: true, + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + teamId: { + type: "string", + description: "Team ID to filter projects by", + }, + first: { + type: "integer", + format: "int32", + description: "Maximum number of results (default 50)", + }, + }, + required: [], + }; + + const result = downgradeSchemaToDraft7(schema); + expect(result.$schema).toBe("http://json-schema.org/draft-07/schema#"); + expect(result.type).toBe("object"); + expect(result.additionalProperties).toBe(true); + expect(result.properties.teamId).toEqual(schema.properties.teamId); + expect(result.properties.first).toEqual(schema.properties.first); + expect(result.required).toEqual([]); + }); + + it("should downgrade schema with merge request properties", () => { + const schema = { + additionalProperties: true, + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + shouldRemoveSourceBranch: { + type: "boolean", + description: "Should remove source branch after merge", + }, + mrIid: { + type: "integer", + format: "int32", + description: "Merge request IID", + }, + mergeCommitMessage: { + type: "string", + description: "Merge commit message", + }, + projectId: { + type: "string", + description: "Project ID or path", + }, + }, + required: ["projectId", "mrIid"], + }; + + const result = downgradeSchemaToDraft7(schema); + expect(result.$schema).toBe("http://json-schema.org/draft-07/schema#"); + expect(result.type).toBe("object"); + expect(result.additionalProperties).toBe(true); + expect(result.properties.shouldRemoveSourceBranch).toEqual( + schema.properties.shouldRemoveSourceBranch, + ); + expect(result.properties.mrIid).toEqual(schema.properties.mrIid); + expect(result.properties.mergeCommitMessage).toEqual( + schema.properties.mergeCommitMessage, + ); + expect(result.properties.projectId).toEqual(schema.properties.projectId); + expect(result.required).toEqual(["projectId", "mrIid"]); + }); + + it("should return Draft 7 schema as-is when already Draft 7", () => { + const schema = { + additionalProperties: true, + type: "object", + $schema: "http://json-schema.org/draft-07/schema#", + properties: { + limit: { + maximum: 100, + description: + "A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 10.", + type: "integer", + minimum: 1, + }, + }, + }; + + const result = downgradeSchemaToDraft7(schema); + // Already Draft 7, should return as-is + expect(result).toBe(schema); + expect(result.$schema).toBe("http://json-schema.org/draft-07/schema#"); + expect(result.properties.limit).toEqual(schema.properties.limit); + }); + + it("should convert empty $defs to definitions and add Draft 7 schema", () => { + const schema = { + $defs: {}, + additionalProperties: true, + type: "object", + properties: { + start_cursor: { + type: "string", + description: + "If supplied, this endpoint will return a page of results starting after the cursor provided. If not supplied, this endpoint will return the first page of results.", + }, + block_id: { + type: "string", + description: "Identifier for a [block](ref:block)", + }, + page_size: { + format: "int32", + description: + "The number of items from the full list desired in the response. Maximum: 100", + default: 100, + type: "integer", + }, + }, + required: ["block_id"], + }; + + const result = downgradeSchemaToDraft7(schema); + expect(result.$schema).toBe("http://json-schema.org/draft-07/schema#"); + expect(result.$defs).toBeUndefined(); + expect(result.definitions).toBeDefined(); + expect(result.definitions).toEqual({}); + expect(result.type).toBe("object"); + expect(result.additionalProperties).toBe(true); + expect(result.properties.start_cursor).toEqual( + schema.properties.start_cursor, + ); + expect(result.properties.block_id).toEqual(schema.properties.block_id); + expect(result.properties.page_size).toEqual(schema.properties.page_size); + expect(result.required).toEqual(["block_id"]); + }); + }); + + describe("JsonForms compatibility", () => { + it("should produce valid Draft 7 schemas that pass isJSONSchemaValid", () => { + const schemas = [ + { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + name: { type: "string" }, + }, + }, + { + $schema: "https://json-schema.org/draft/2019-09/schema", + type: "object", + $defs: { + address: { type: "object" }, + }, + }, + { + type: "object", + $defs: { + test: { type: "string" }, + }, + }, + { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + teamId: { type: "string" }, + first: { type: "integer" }, + }, + unevaluatedProperties: false, + }, + { + $schema: "http://json-schema.org/draft-04/schema#", + type: "object", + properties: { + name: { type: "string" }, + }, + }, + { + $schema: "http://json-schema.org/draft-06/schema#", + type: "object", + properties: { + value: { type: "number" }, + }, + }, + ]; + + schemas.forEach((schema) => { + const downgraded = downgradeSchemaToDraft7(schema); + const isValid = isJSONSchemaValid(downgraded); + expect(isValid).toBe(true); + }); + }); + + it("should convert Draft 04 and Draft 06 schemas to Draft 7 for JsonForms compatibility", () => { + const draft04Schema = { + $schema: "http://json-schema.org/draft-04/schema#", + type: "object", + properties: { + name: { type: "string" }, + age: { type: "integer" }, + }, + required: ["name"], + }; + + const draft06Schema = { + $schema: "http://json-schema.org/draft-06/schema#", + type: "object", + properties: { + email: { type: "string", format: "email" }, + }, + }; + + const downgraded04 = downgradeSchemaToDraft7(draft04Schema); + const downgraded06 = downgradeSchemaToDraft7(draft06Schema); + + // Both should be Draft 7 + expect(downgraded04.$schema).toBe( + "http://json-schema.org/draft-07/schema#", + ); + expect(downgraded06.$schema).toBe( + "http://json-schema.org/draft-07/schema#", + ); + + // Both should pass JsonForms validation + expect(isJSONSchemaValid(downgraded04)).toBe(true); + expect(isJSONSchemaValid(downgraded06)).toBe(true); + }); + + it("should validate all real-world example schemas with JsonForms validator", () => { + const exampleSchemas = [ + { + additionalProperties: true, + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + teamId: { + type: "string", + description: "Team ID to filter projects by", + }, + first: { + type: "integer", + format: "int32", + description: "Maximum number of results (default 50)", + }, + }, + required: [], + }, + { + additionalProperties: true, + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + shouldRemoveSourceBranch: { + type: "boolean", + description: "Should remove source branch after merge", + }, + mrIid: { + type: "integer", + format: "int32", + description: "Merge request IID", + }, + mergeCommitMessage: { + type: "string", + description: "Merge commit message", + }, + projectId: { + type: "string", + description: "Project ID or path", + }, + }, + required: ["projectId", "mrIid"], + }, + { + $defs: {}, + additionalProperties: true, + type: "object", + properties: { + start_cursor: { + type: "string", + description: + "If supplied, this endpoint will return a page of results starting after the cursor provided. If not supplied, this endpoint will return the first page of results.", + }, + block_id: { + type: "string", + description: "Identifier for a [block](ref:block)", + }, + page_size: { + format: "int32", + description: + "The number of items from the full list desired in the response. Maximum: 100", + default: 100, + type: "integer", + }, + }, + required: ["block_id"], + }, + ]; + + exampleSchemas.forEach((schema) => { + const downgraded = downgradeSchemaToDraft7(schema); + const isValid = isJSONSchemaValid(downgraded); + expect(isValid).toBe(true); + }); + }); + + it("should handle complex nested schemas and validate with JsonForms", () => { + const complexSchema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + user: { + type: "object", + properties: { + name: { type: "string" }, + address: { $ref: "#/$defs/address" }, + }, + }, + items: { + type: "array", + items: { $ref: "#/$defs/item" }, + unevaluatedItems: false, + }, + }, + allOf: [ + { type: "object" }, + { + anyOf: [{ type: "object", properties: { x: { type: "number" } } }], + }, + ], + $defs: { + address: { + type: "object", + properties: { + street: { type: "string" }, + }, + unevaluatedProperties: false, + }, + item: { + type: "object", + properties: { + value: { type: "string" }, + }, + }, + }, + unevaluatedProperties: false, + dependentRequired: { x: ["y"] }, + minContains: 1, + }; + + const downgraded = downgradeSchemaToDraft7(complexSchema); + const isValid = isJSONSchemaValid(downgraded); + expect(isValid).toBe(true); + }); + + it("should validate schemas with all supported Draft 7 features", () => { + const draft7CompatibleSchema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + stringProp: { type: "string", minLength: 1, maxLength: 100 }, + numberProp: { type: "number", minimum: 0, maximum: 100 }, + integerProp: { type: "integer", multipleOf: 2 }, + booleanProp: { type: "boolean" }, + arrayProp: { + type: "array", + items: { type: "string" }, + minItems: 1, + maxItems: 10, + uniqueItems: true, + }, + objectProp: { + type: "object", + properties: { + nested: { type: "string" }, + }, + required: ["nested"], + }, + }, + required: ["stringProp"], + allOf: [{ type: "object" }], + anyOf: [{ type: "object" }], + oneOf: [{ type: "object" }], + not: { type: "null" }, + if: { properties: { type: { const: "conditional" } } }, + then: { properties: { value: { type: "string" } } }, + else: { properties: { value: { type: "number" } } }, + $defs: { + reusable: { type: "string" }, + }, + }; + + const downgraded = downgradeSchemaToDraft7(draft7CompatibleSchema); + const isValid = isJSONSchemaValid(downgraded); + expect(isValid).toBe(true); + }); + }); +}); diff --git a/ui-next/src/utils/__tests__/object.test.ts b/ui-next/src/utils/__tests__/object.test.ts new file mode 100644 index 0000000000..100899c9f2 --- /dev/null +++ b/ui-next/src/utils/__tests__/object.test.ts @@ -0,0 +1,24 @@ +import { replaceValues } from "../object"; + +describe("replaceValues", () => { + it("should replace values in an object", () => { + const obj = { a: "a", b: "b", c: "c" }; + const expected = { a: "a", b: "b", c: "d" }; + expect(replaceValues(obj, "c", "d")).toEqual(expected); + }); + it("should replace values in an object with nested objects", () => { + const obj = { a: "a", b: "b", c: { d: "d", e: "e" } }; + const expected = { a: "a", b: "b", c: { d: "d", e: "f" } }; + expect(replaceValues(obj, "e", "f")).toEqual(expected); + }); + it("should replace values in an object with nested arrays", () => { + const obj = { a: "a", b: "b", c: ["d", "e"] }; + const expected = { a: "a", b: "b", c: ["d", "f"] }; + expect(replaceValues(obj, "e", "f")).toEqual(expected); + }); + it("should replace values in an object with nested arrays and objects", () => { + const obj = { a: "a", b: "b", c: ["d", { e: "e" }] }; + const expected = { a: "a", b: "b", c: ["d", { e: "f" }] }; + expect(replaceValues(obj, "e", "f")).toEqual(expected); + }); +}); diff --git a/ui-next/src/utils/__tests__/string.test.ts b/ui-next/src/utils/__tests__/string.test.ts new file mode 100644 index 0000000000..ac9296e07a --- /dev/null +++ b/ui-next/src/utils/__tests__/string.test.ts @@ -0,0 +1,39 @@ +import { getSequentiallySuffix } from "utils/strings"; + +const cases = [ + { + name: "test", + refNames: ["test_1", "test_2", "test_12"], + expected: { + name: "test_3", + taskReferenceName: "test_3", + }, + }, + { + name: "task-name", + refNames: ["task-name_4", "task-name_5", "task-name_1"], + expected: { + name: "task-name_2", + taskReferenceName: "task-name_2", + }, + }, + { + name: "task-name", + refNames: [], + expected: { + name: "task-name", + taskReferenceName: "task-name", + }, + }, +]; + +describe("Get sequential name", () => { + test.each(cases)( + "given '$name' and $refNames as arguments, returns $expected", + ({ name, refNames, expected }) => { + const result = getSequentiallySuffix({ name, refNames }); + + expect(result).toMatchObject(expected); + }, + ); +}); diff --git a/ui-next/src/utils/__tests__/toMaybeQueryString.test.ts b/ui-next/src/utils/__tests__/toMaybeQueryString.test.ts new file mode 100644 index 0000000000..b023593f58 --- /dev/null +++ b/ui-next/src/utils/__tests__/toMaybeQueryString.test.ts @@ -0,0 +1,38 @@ +import { urlWithQueryParameters } from "../toMaybeQueryString"; + +describe("urlWithQueryParameters", () => { + it("should append query parameters with ? when URL has no existing parameters", () => { + const url = "https://example.com/api"; + const params = { key: "value", another: "param" }; + expect(urlWithQueryParameters(url, params)).toBe( + "https://example.com/api?key=value&another=param", + ); + }); + + it("should append query parameters with & when URL already has parameters", () => { + const url = "https://example.com/api?existing=true"; + const params = { key: "value", another: "param" }; + expect(urlWithQueryParameters(url, params)).toBe( + "https://example.com/api?existing=true&key=value&another=param", + ); + }); + + it("should handle empty parameters object", () => { + const url = "https://example.com/api"; + const params = {}; + expect(urlWithQueryParameters(url, params)).toBe("https://example.com/api"); + }); + + it("should handle undefined values in parameters", () => { + const url = "https://example.com/api"; + const params = { key: "value", empty: undefined }; + expect(urlWithQueryParameters(url, params)).toBe( + "https://example.com/api?key=value", + ); + }); + it("should handle empty url", () => { + const url = ""; + const params = {}; + expect(urlWithQueryParameters(url, params)).toBe(""); + }); +}); diff --git a/ui-next/src/utils/__tests__/typeHelpers.test.ts b/ui-next/src/utils/__tests__/typeHelpers.test.ts new file mode 100644 index 0000000000..051b9690ac --- /dev/null +++ b/ui-next/src/utils/__tests__/typeHelpers.test.ts @@ -0,0 +1,154 @@ +import { + FIELD_TYPE_BOOLEAN, + FIELD_TYPE_NULL, + FIELD_TYPE_NUMBER, + FIELD_TYPE_OBJECT, + FIELD_TYPE_STRING, +} from "types/common"; +import { castToType, checkCoerceTypeError, inferType } from "utils/helpers"; + +const inferTypeCases = [ + { + value: 123, + expected: FIELD_TYPE_NUMBER, + }, + { + value: null, + expected: FIELD_TYPE_NULL, + }, + { + value: "test", + expected: FIELD_TYPE_STRING, + }, + { + value: false, + expected: FIELD_TYPE_BOOLEAN, + }, + { + value: { key: "value" }, + expected: FIELD_TYPE_OBJECT, + }, +]; + +describe("Check inferType function", () => { + test.each(inferTypeCases)( + "given '$value' as argument, returns $expected", + ({ value, expected }) => { + const result = inferType(value); + + expect(result).toBe(expected); + }, + ); +}); + +const castToTypeCases = [ + { + value: 123, + expected: 123, + }, + { + value: 0, + expected: 0, + }, + { + value: "123.321", + expected: "123.321", + }, + { + value: null, + expected: null, + }, + { + value: "test", + expected: "test", + }, + { + value: false, + expected: false, + }, + { + value: '{ key: "value" }', + expected: '{ key: "value" }', + }, + { + value: { key: "value" }, + expected: {}, + }, + { + value: "", + expected: "", + }, + { + value: "[1,2]", + expected: "[1,2]", + }, +]; + +describe("Check castToType function", () => { + test.each(castToTypeCases)( + "given '$value' as argument, returns '$expected'", + ({ value, expected }) => { + const result = castToType(value, inferType(value)); + + // Use toMatchObject for objects (which also works for primitives) + // or toBe for primitives - determined outside conditional + const isObjectExpected = expected && typeof expected === "object"; + + // Always make both assertions - one will be the actual check, one will be trivial + expect(isObjectExpected ? result : null).toMatchObject( + isObjectExpected ? expected : {}, + ); + expect(isObjectExpected || result === expected).toBeTruthy(); + }, + ); +}); + +// true: has error +const checkCoerceTypeErrorCases = [ + { + value: 123, + coerceTo: "integer", + expected: false, + }, + { + value: 123.123, + coerceTo: "integer", + expected: true, + }, + { + value: "123", + coerceTo: "integer", + expected: false, + }, + { + value: 123.321, + coerceTo: "double", + expected: false, + }, + { + value: "${someVariables}", + coerceTo: "double", + expected: false, + }, + { + value: "123.321a", + coerceTo: "double", + expected: true, + }, + { + value: "123.321", + coerceTo: "string", + expected: false, + }, +]; + +describe("Check checkCoerceTypeError function", () => { + test.each(checkCoerceTypeErrorCases)( + "given '$value' as argument, returns $expected", + ({ value, coerceTo, expected }) => { + const result = checkCoerceTypeError({ value, coerceTo }); + + expect(result).toBe(expected); + }, + ); +}); diff --git a/ui-next/src/utils/__tests__/utils.test.ts b/ui-next/src/utils/__tests__/utils.test.ts new file mode 100644 index 0000000000..3bbdc61bb1 --- /dev/null +++ b/ui-next/src/utils/__tests__/utils.test.ts @@ -0,0 +1,100 @@ +import { getErrors } from "utils"; + +const mockedHeaders = new Map([["content-type", "application/json"]]); +const headers = { + append: (name: string, value: string) => { + mockedHeaders.set(name, value); + return mockedHeaders; + }, + delete: (name: string) => { + mockedHeaders.delete(name); + }, + get: (name: string) => mockedHeaders.get(name), + has: (name: string) => mockedHeaders.has(name), + set: (name: string, value: string) => mockedHeaders.set(name, value), + forEach: mockedHeaders.forEach, +}; + +describe("getErrors", () => { + it("should return all errors", async () => { + const response = { + json: async () => { + return { + message: "Bad request", + validationErrors: [ + { + path: "registerTaskDef.taskDefinitions[0].ownerEmail", + message: "ownerEmail cannot be empty", + }, + { + path: "registerTaskDef.taskDefinitions[0].name", + message: "name cannot be empty", + }, + ], + }; + }, + headers: headers as unknown as Headers, + clone: () => ({ ...response }), + status: 400, + } as Response; + const errors = await getErrors(response); + expect(errors).toMatchObject({ + "registerTaskDef.taskDefinitions[0].ownerEmail": + "ownerEmail cannot be empty", + "registerTaskDef.taskDefinitions[0].name": "name cannot be empty", + }); + }); + + it("should return the error message", async () => { + const response = { + json: async () => { + return { + message: "Bad request", + }; + }, + headers: headers as unknown as Headers, + clone: () => ({ ...response }), + status: 400, + } as Response; + const errors = await getErrors(response); + expect(errors).toMatchObject({ + message: "Bad request", + }); + }); + + it("Should return default error message, if error could not be identified", async () => { + const response = { + json: async () => { + return { + name: "taskDef1", + }; + }, + clone: () => ({ ...response }), + status: 502, + statusText: "Bad Gateway", + } as Response; + const errors = await getErrors(response); + expect(errors).toMatchObject({ + message: `Error performing action. error number: 502 Bad Gateway`, + }); + }); + + it("Should be able to change error handler to custom function", async () => { + const response = { + json: async () => { + return { + name: "taskDef1", + }; + }, + clone: () => ({ ...response }), + status: 502, + statusText: "Bad Gateway", + } as Response; + const errors = await getErrors(response, () => ({ + message: "Hi im custom error handler", + })); + expect(errors).toMatchObject({ + message: "Hi im custom error handler", + }); + }); +}); diff --git a/ui-next/src/utils/__tests__/workflow.test.ts b/ui-next/src/utils/__tests__/workflow.test.ts new file mode 100644 index 0000000000..809dc38953 --- /dev/null +++ b/ui-next/src/utils/__tests__/workflow.test.ts @@ -0,0 +1,145 @@ +import { scanTasksForDependenciesInWorkflow } from "../workflow"; + +describe("scanTasksForDependenciesInWorkflow", () => { + it("should return empty dependencies for an empty workflow", () => { + const workflow = { + tasks: [], + }; + const result = scanTasksForDependenciesInWorkflow(workflow as any); + expect(result).toEqual({ + integrationNames: [], + promptNames: [], + userFormsNameVersion: [], + schemas: [], + secrets: [], + env: [], + workflowName: undefined, + workflowVersion: undefined, + }); + }); + + it("should extract LLM integration, prompt, secrets, and env from tasks", () => { + const workflow = { + tasks: [ + { + type: "LLM_TEXT_COMPLETE", + inputParameters: { + llmProvider: "openai", + promptName: "myPrompt", + secretField: "${workflow.secrets.API_KEY}", + envField: "${workflow.env.MY_ENV}", + }, + }, + ], + }; + const result = scanTasksForDependenciesInWorkflow(workflow as any); + expect(result.integrationNames).toContain("openai"); + expect(result.promptNames).toContain("myPrompt"); + expect(result.secrets).toContain("${workflow.secrets.API_KEY}"); + expect(result.env).toContain("${workflow.env.MY_ENV}"); + }); + + it("should extract user form name/version from human tasks", () => { + const workflow = { + tasks: [ + { + type: "HUMAN", + inputParameters: { + __humanTaskDefinition: { + userFormTemplate: { name: "formA", version: 2 }, + }, + }, + }, + ], + }; + const result = scanTasksForDependenciesInWorkflow(workflow as any); + expect(result.userFormsNameVersion).toEqual([ + { name: "formA", version: "2" }, + ]); + }); + + it("should extract schemas from task definitions", () => { + const workflow = { + tasks: [ + { + type: "SIMPLE", + inputParameters: {}, + taskDefinition: { + inputSchema: { name: "inputSchema", version: 1 }, + outputSchema: { name: "outputSchema", version: 2 }, + }, + }, + ], + }; + const result = scanTasksForDependenciesInWorkflow(workflow as any); + expect(result.schemas).toEqual([ + { name: "inputSchema", version: "1" }, + { name: "outputSchema", version: "2" }, + ]); + }); + + it("should extract workflow-level schemas and outputParameters secrets/env", () => { + const workflow = { + name: "wf1", + version: 3, + tasks: [], + inputSchema: { name: "wfInput", version: 1 }, + outputSchema: { name: "wfOutput", version: 2 }, + outputParameters: { + secret: "${workflow.secrets.SECRET1}", + env: "${workflow.env.ENV1}", + }, + }; + const result = scanTasksForDependenciesInWorkflow(workflow as any); + expect(result.schemas).toEqual([ + { name: "wfInput", version: "1" }, + { name: "wfOutput", version: "2" }, + ]); + expect(result.secrets).toContain("${workflow.secrets.SECRET1}"); + expect(result.env).toContain("${workflow.env.ENV1}"); + expect(result.workflowName).toBe("wf1"); + expect(result.workflowVersion).toBe(3); + }); + + it("should deduplicate user forms and schemas by name/version", () => { + const workflow = { + tasks: [ + { + type: "HUMAN", + inputParameters: { + __humanTaskDefinition: { + userFormTemplate: { name: "formA", version: 1 }, + }, + }, + }, + { + type: "HUMAN", + inputParameters: { + __humanTaskDefinition: { + userFormTemplate: { name: "formA", version: 1 }, + }, + }, + }, + { + type: "SIMPLE", + inputParameters: {}, + taskDefinition: { + inputSchema: { name: "schemaA", version: 1 }, + }, + }, + { + type: "SIMPLE", + inputParameters: {}, + taskDefinition: { + inputSchema: { name: "schemaA", version: 1 }, + }, + }, + ], + }; + const result = scanTasksForDependenciesInWorkflow(workflow as any); + expect(result.userFormsNameVersion).toEqual([ + { name: "formA", version: "1" }, + ]); + expect(result.schemas).toEqual([{ name: "schemaA", version: "1" }]); + }); +}); diff --git a/ui-next/src/utils/accessControl.ts b/ui-next/src/utils/accessControl.ts new file mode 100644 index 0000000000..e9ffa7f3c9 --- /dev/null +++ b/ui-next/src/utils/accessControl.ts @@ -0,0 +1,59 @@ +import { AccessRole } from "types/User"; + +export interface UserInfo { + roles?: AccessRole[]; + groups?: any[]; +} + +const hasAnyRole = ( + userInfo: UserInfo | undefined | null, + allowedRoles: string[], +) => { + if (!userInfo) { + return false; + } + + const hasAllowedRoles = (roles?: any[]) => + roles?.find((role) => allowedRoles.includes(role.name)); + + if (hasAllowedRoles(userInfo.roles)) { + return true; + } + + if (userInfo.groups?.find((group) => hasAllowedRoles(group.roles))) { + return true; + } + + return false; +}; + +export const accessControl = { + hasUserManagement: (userInfo?: UserInfo) => { + return hasAnyRole(userInfo, ["ADMIN"]); + }, + hasApplicationManagement: (userInfo?: UserInfo) => { + return hasAnyRole(userInfo, ["USER", "ADMIN"]); + }, + hasOnlyReadOnlyAccess: (userInfo?: UserInfo) => { + if ( + hasAnyRole(userInfo, [ + "ADMIN", + "USER", + "METADATA_MANAGER", + "WORKFLOW_MANAGER", + ]) + ) { + return false; + } + return hasAnyRole(userInfo, ["USER_READ_ONLY"]); + }, + hasAnyRole, +}; + +export enum Role { + ADMIN = "ADMIN", + USER = "USER", + METADATA_MANAGER = "METADATA_MANAGER", + WORKFLOW_MANAGER = "WORKFLOW_MANAGER", + USER_READ_ONLY = "USER_READ_ONLY", +} diff --git a/ui-next/src/utils/array.ts b/ui-next/src/utils/array.ts new file mode 100644 index 0000000000..94b58b07f1 --- /dev/null +++ b/ui-next/src/utils/array.ts @@ -0,0 +1,24 @@ +export const adjust = (idx: number, aplFun: () => T, sourceArray: T[]) => + Object.assign([], sourceArray, { [idx]: aplFun() }); + +/** + * Takes an index and a count removes from index count elements of array + * + * @param {*} idx + * @param {*} count + * @param {*} sourceArray + * @returns + */ +export const remove = (idx: number, count: number, sourceArray: Array) => { + const arrayCopy = sourceArray.slice(); + arrayCopy.splice(idx, count); + return arrayCopy; +}; + +export const insert = (index: number, newItem: T, arr: T[]) => + arr.slice(0, index).concat(newItem).concat(arr.slice(index)); + +export const cartesianProduct = ( + a: Array, + b: Array, +): Array<[TA, TB]> => a.flatMap((va) => b.map((vb): [TA, TB] => [va, vb])); diff --git a/ui-next/src/utils/checkPathFlag.ts b/ui-next/src/utils/checkPathFlag.ts new file mode 100644 index 0000000000..967488db0d --- /dev/null +++ b/ui-next/src/utils/checkPathFlag.ts @@ -0,0 +1,51 @@ +import { FEATURES, featureFlags } from "utils/flags"; + +type PathFlagMap = { + [key: string]: any; +}; + +const pathFlagMap: PathFlagMap = { + "/workflowDef": true, + "/taskDef": true, + "/get-started": featureFlags.isEnabled(FEATURES.SHOW_GET_STARTED_PAGE), + "/scheduleDef": featureFlags.isEnabled(FEATURES.SCHEDULER), + "/schedulerExecs": featureFlags.isEnabled(FEATURES.SCHEDULER), + "/newScheduleDef": featureFlags.isEnabled(FEATURES.SCHEDULER), + "/secrets": featureFlags.isEnabled(FEATURES.SECRETS), + "/human": featureFlags.isEnabled(FEATURES.HUMAN_TASK), + "/configure-webhook": featureFlags.isEnabled(FEATURES.WEBHOOKS), + "/newWebhook": featureFlags.isEnabled(FEATURES.WEBHOOKS), + "/userManagement": featureFlags.isEnabled(FEATURES.RBAC), + "/groupManagement": featureFlags.isEnabled(FEATURES.RBAC), + "/ai_prompts": featureFlags.isEnabled(FEATURES.INTEGRATIONS), + "/integrations": featureFlags.isEnabled(FEATURES.INTEGRATIONS), + "/": true, +}; + +const pathFlagMapWithoutSKU: PathFlagMap = { + "/scheduleDef": featureFlags.isEnabled(FEATURES.SCHEDULER), + "/schedulerExecs": featureFlags.isEnabled(FEATURES.SCHEDULER), + "/newScheduleDef": featureFlags.isEnabled(FEATURES.SCHEDULER), + "/human": featureFlags.isEnabled(FEATURES.HUMAN_TASK), + "/ai_prompts": featureFlags.isEnabled(FEATURES.INTEGRATIONS), + "/integrations": featureFlags.isEnabled(FEATURES.INTEGRATIONS), + "/remote-services": featureFlags.isEnabled(FEATURES.REMOTE_SERVICES), + "/newRemoteServiceDef": featureFlags.isEnabled(FEATURES.REMOTE_SERVICES), + "/": true, +}; + +export const checkPathFlag = (path: string) => { + if (!featureFlags.isEnabled(FEATURES.SKU_ENABLED)) { + for (const key in pathFlagMapWithoutSKU) { + if (path.startsWith(key)) { + return pathFlagMapWithoutSKU[key]; + } + } + } + for (const key in pathFlagMap) { + if (path.startsWith(key)) { + return pathFlagMap[key]; + } + } + return false; +}; diff --git a/ui-next/src/utils/cloudTemplates.ts b/ui-next/src/utils/cloudTemplates.ts new file mode 100644 index 0000000000..7a46f7f09b --- /dev/null +++ b/ui-next/src/utils/cloudTemplates.ts @@ -0,0 +1,825 @@ +import { AuthHeaders, ErrorObj } from "types/common"; +import { + CloudTemplateType, + CloudTemplateTypeV1, + CloudTemplateTypeV2, + IntegrationAndModel, +} from "types/CloudTemplateType"; +import { logger } from "./logger"; +import { getErrorMessage, tryFunc, tryToJson } from "./utils"; +import { WorkflowDef } from "types/WorkflowDef"; +import { fetchWithContext } from "plugins/fetch"; +import { CommonTaskDef } from "types/TaskType"; +import _zip from "lodash/zip"; +import _flow from "lodash/flow"; +import { featureFlags, FEATURES } from "./flags"; +import { replaceValues } from "./object"; +import { HumanTemplate } from "types/HumanTaskTypes"; +import { SchemaDefinition } from "types/SchemaDefinition"; +import { + SchemaResult, + TaskResult, + HumanTemplateResult, + WorkflowResult, + IntegrationAndModelResult, + ModelResult, + IntegrationResult, + PromptResult, +} from "types/CloudTemplateResults"; +import { IntegrationI, ModelDto } from "types/Integrations"; +import { PromptDef } from "types/Prompts"; +import { toMaybeQueryString } from "./toMaybeQueryString"; + +const CLOUD_CALL_STORAGE_KEY = "cloudTemplates"; +const WF_TEMPLATE_URL_PREFIX = "ct-wf-"; + +const TASK_TEMPLATE_URL_PREFIX = "ct-task-"; + +// https://beta-saas.orkesconductor.com/api/templates +// https://cloud.orkes.io/api/templates + +// removing quotes from the string +const cloudTemplatesSourceFlagValue = featureFlags + .getValue(FEATURES.CLOUD_TEMPLATES_SOURCE) + ?.replace(/['"]/g, ""); + +const CLOUD_URL = + cloudTemplatesSourceFlagValue ?? + "https://raw.githubusercontent.com/conductor-oss/awesome-conductor-apps/refs/heads/main/templates.json"; + +export const justName = ({ name }: { name: string }) => name; + +/** + * Validates that a template has all the required fields that TemplateCard needs to render properly. + * This includes: id, title, description, category, tags (as an array), and version >= 2. + */ +export const isValidTemplate = ( + template: CloudTemplateType | null | undefined, +): boolean => { + if (!template) return false; + + return ( + typeof template.id === "string" && + template.id.length > 0 && + typeof template.title === "string" && + template.title.length > 0 && + typeof template.description === "string" && + typeof template.category === "string" && + template.category.length > 0 && + Array.isArray(template.tags) && + typeof template.version === "number" && + template.version >= 2 + ); +}; + +// const fetchContext = fetchContextNonHook(); + +export const fetchCloudTemplates = async (): Promise<{ + cloudTemplates: CloudTemplateType[]; +}> => { + try { + const response = await fetch(CLOUD_URL); + const result = await response.text(); + + const relevantTemplates = JSON.parse(result); + + localStorage.setItem(CLOUD_CALL_STORAGE_KEY, result); + + return { cloudTemplates: relevantTemplates }; + } catch (error) { + logger.error(error); + logger.log("Using cached cloud templates"); + const cached = localStorage.getItem(CLOUD_CALL_STORAGE_KEY); + return { cloudTemplates: tryToJson(cached) || [] }; + } +}; + +export const fetchCloudTemplatesPreferCached = async (): Promise<{ + cloudTemplates: CloudTemplateType[]; +}> => { + const cached = localStorage.getItem(CLOUD_CALL_STORAGE_KEY); + if (cached) { + return { cloudTemplates: tryToJson(cached) || [] }; + } + return fetchCloudTemplates(); +}; +// FIXME this code is repeated makes no sense at all + +const fetchWorkflowAndCatch = async ( + workflowPath?: string, + keyPrefix = WF_TEMPLATE_URL_PREFIX, +) => { + try { + if (workflowPath) { + const workflowResponse = await fetch(workflowPath); + const workflowResult = await workflowResponse.json(); + + localStorage.setItem( + `${keyPrefix}${workflowPath}`, + JSON.stringify(workflowResult), + ); + + return workflowResult; + } + + return []; + } catch { + return tryToJson(localStorage.getItem(`${keyPrefix}${workflowPath}`)) || []; + } +}; + +const fetchTask = async (taskPath?: string) => { + try { + if (taskPath) { + const taskResponse = await fetch(taskPath); + const taskResult = await taskResponse.json(); + + localStorage.setItem( + `${TASK_TEMPLATE_URL_PREFIX}${taskPath}`, + taskResult, + ); + + return taskResult; + } + + return []; + } catch { + return ( + tryToJson( + localStorage.getItem(`${TASK_TEMPLATE_URL_PREFIX}${taskPath}`), + ) || [] + ); + } +}; + +const fetchUserForms = async (userFormsPath?: string) => { + try { + if (userFormsPath) { + const userFormsResponse = await fetch(userFormsPath); + const userFormsResult = await userFormsResponse.json(); + + return userFormsResult; + } + + return []; + } catch { + logger.error("Failed to fetch User Forms"); + return []; + } +}; + +const fetchSchemas = async (schemasPath?: string) => { + try { + if (schemasPath) { + const schemasResponse = await fetch(schemasPath); + const schemasResult = await schemasResponse.json(); + + return schemasResult; + } + + return []; + } catch { + logger.error("Failed to fetch Schemas"); + return []; + } +}; + +const fetchIntegrationAndModels = async ( + integrationsAndModelsPath?: string, +): Promise => { + try { + if (integrationsAndModelsPath) { + const integrationsAndModelResponse = await fetch( + integrationsAndModelsPath, + ); + const integrationsAndModels = await integrationsAndModelResponse.json(); + + return integrationsAndModels; + } + + return []; + } catch { + return ( + tryToJson( + localStorage.getItem( + `${TASK_TEMPLATE_URL_PREFIX}${integrationsAndModelsPath}`, + ), + ) || [] + ); + } +}; + +const fetchPrompts = async (promptsPath?: string) => { + try { + if (promptsPath) { + const promptsResponse = await fetch(promptsPath); + const promptsResult = await promptsResponse.json(); + + return promptsResult; + } + logger.info("prompts path not defined"); + return []; + } catch { + logger.error("Failed to fetch Prompts"); + return []; + } +}; +// end of repeated code +export type ImportSummary = { + workflowResponse: WorkflowDef[]; + taskResponse: CommonTaskDef[]; + userFormsResponse: HumanTemplate[]; + schemasResponse: SchemaDefinition[]; + integrationsAndModelsResponse: IntegrationAndModel[]; + promptsResponse: PromptDef[]; +}; + +const isCloudTemplateV1 = ( + card: CloudTemplateType, +): card is CloudTemplateTypeV1 => { + return card.version === 1; +}; + +const isCloudTemplateV2 = ( + card: CloudTemplateType, +): card is CloudTemplateTypeV2 => { + return card.version === 2; +}; + +export const fetchWorkflowWithDependencies = async ( + selectedCard: CloudTemplateType, +): Promise => { + const empty = { + workflowResponse: [], + taskResponse: [], + userFormsResponse: [], + schemasResponse: [], + integrationsAndModelsResponse: [], + promptsResponse: [], + }; + if (!selectedCard) { + return empty; + } + if (isCloudTemplateV1(selectedCard)) { + return fetchWorkflowWithDependenciesV1(selectedCard); + } + if (isCloudTemplateV2(selectedCard)) { + return fetchWorkflowWithDependenciesV2(selectedCard); + } + return empty; +}; + +export const fetchWorkflowWithDependenciesV2 = ( + selectedCard: CloudTemplateTypeV2, +): ImportSummary => { + return { + workflowResponse: (selectedCard?.workflowDefinitions ?? + []) as unknown as WorkflowDef[], + taskResponse: (selectedCard?.taskDefinitions ?? + []) as unknown as CommonTaskDef[], + userFormsResponse: (selectedCard?.userForms ?? + []) as unknown as HumanTemplate[], + schemasResponse: (selectedCard?.schemas ?? + []) as unknown as SchemaDefinition[], + integrationsAndModelsResponse: (selectedCard?.integrationsWithModels ?? + []) as unknown as IntegrationAndModel[], + promptsResponse: (selectedCard?.prompts ?? []) as PromptDef[], + }; +}; + +export const fetchWorkflowWithDependenciesV1 = async ( + selectedCard: CloudTemplateTypeV1, +) => { + return tryFunc({ + fn: async () => { + const workflowPath = + selectedCard?.workflowTemplateDefLink ?? + selectedCard?.workflowDefinitionGithubLink; + const taskPath = + selectedCard?.taskTemplateDefsLink ?? + selectedCard?.taskDefinitionsGithubLink; + const userFormsPath = + selectedCard?.userFormTemplateDefLink ?? + selectedCard?.userFormsGithubLink; + const schemasPath = + selectedCard?.schemaDefTemplateLink ?? selectedCard?.schemasGithubLink; + const integrationsAndModels = + selectedCard?.integrationAndModelsGithubLink; + const promptsPath = selectedCard?.promptsGithubLink; + + const [ + workflowData, + taskData, + userFormsData, + schemasData, + integrationsAndModelsData, + promptsData, + ] = await Promise.all([ + fetchWorkflowAndCatch(workflowPath), + fetchTask(taskPath), + fetchUserForms(userFormsPath), + fetchSchemas(schemasPath), + fetchIntegrationAndModels(integrationsAndModels), + fetchPrompts(promptsPath), + ]); + + return { + workflowResponse: workflowData ?? [], + taskResponse: taskData ?? [], + userFormsResponse: userFormsData ?? [], + schemasResponse: schemasData ?? [], + integrationsAndModelsResponse: integrationsAndModelsData ?? [], + promptsResponse: promptsData ?? [], + } as const; + }, + customError: { + message: "Fetching workflows and tasks failed!", + }, + showCustomError: false, + }); +}; + +export const importWorkflow = async ( + context: { authHeaders: AuthHeaders; workflowNames: string[] }, + workflowDefinition: WorkflowDef, +) => { + const { authHeaders, workflowNames } = context; + if (workflowNames.includes(workflowDefinition.name)) { + return { + workflow: workflowDefinition, + success: false, + message: "Workflow already exists", + }; + } + try { + await fetchWithContext( + "/metadata/workflow?overwrite=true", + {}, + { + method: "POST", + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + body: JSON.stringify(workflowDefinition), + }, + ); + return { workflow: workflowDefinition, success: true }; + } catch (error: any) { + const errorMessage = await getErrorMessage(error); + return { + workflow: workflowDefinition, + success: false, + message: errorMessage ?? "Saving Failed", + }; + } +}; + +export const importTask = async ( + context: { authHeaders: AuthHeaders; taskNames: string[] }, + modifiedTaskDefinition: CommonTaskDef, +) => { + const { authHeaders, taskNames } = context; + if (taskNames.includes(modifiedTaskDefinition.name)) { + return { + task: modifiedTaskDefinition, + success: false, + message: "Task already exists", + }; + } + try { + const stringDefinition = JSON.stringify(modifiedTaskDefinition, null, 2); + const body = `[${stringDefinition}]`; + + await fetchWithContext( + "/metadata/taskdefs", + {}, + { + method: "POST", + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + body, + }, + ); + + return { task: modifiedTaskDefinition, success: true }; + } catch (error: any) { + const errorMessage = await getErrorMessage(error); + return { + task: modifiedTaskDefinition, + success: false, + message: errorMessage ?? "Saving failed", + }; + } +}; + +export const importUserForm = async ( + context: { authHeaders: AuthHeaders }, + userFormDefinition: HumanTemplate, + userFormNames: string[], +) => { + const { authHeaders } = context; + if (userFormNames.includes(userFormDefinition.name)) { + return { + userForm: userFormDefinition, + success: false, + message: "User form already exists", + }; + } + try { + await fetchWithContext( + "/human/template", + {}, + { + method: "POST", + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + body: JSON.stringify(userFormDefinition), + }, + ); + + return { userForm: userFormDefinition, success: true }; + } catch (error: any) { + const errorMessage = await getErrorMessage(error); + return { + userForm: userFormDefinition, + success: false, + message: errorMessage ?? "Saving failed", + }; + } +}; + +export const importSchemas = async ( + context: { authHeaders: AuthHeaders }, + schemasDefinition: SchemaDefinition, + schemasNames: string[], +) => { + const { authHeaders } = context; + if (schemasNames.includes(schemasDefinition.name)) { + return { + schema: schemasDefinition, + success: false, + message: "Schema already exists", + }; + } + try { + await fetchWithContext( + "/schema", + {}, + { + method: "POST", + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + body: JSON.stringify(schemasDefinition), + }, + ); + + return { schema: schemasDefinition, success: true }; + } catch (error: any) { + const errorMessage = await getErrorMessage(error); + return { + schema: schemasDefinition, + success: false, + message: errorMessage ?? "Saving failed", + }; + } +}; + +const importIntegration = async ( + context: { authHeaders: AuthHeaders }, + integration: IntegrationI, +): Promise => { + const { authHeaders } = context; + try { + await fetchWithContext( + `/integrations/provider/${integration.name}`, + {}, + { + method: "POST", + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + body: JSON.stringify(integration), + }, + ); + return { + success: true, + integration, + }; + } catch (error: any) { + const errorMessage = await getErrorMessage(error); + return { + integration: integration, + success: false, + message: errorMessage ?? "Saving failed", + }; + } +}; + +const importModel = async ( + context: { authHeaders: AuthHeaders }, + model: ModelDto, +): Promise => { + const { authHeaders } = context; + try { + await fetchWithContext( + `/integrations/provider/${model.integrationName}/integration/${model.api}`, + {}, + { + method: "POST", + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + body: JSON.stringify(model), + }, + ); + return { + model: model, + success: true, + }; + } catch (error: any) { + const errorMessage = await getErrorMessage(error); + return { + model: model, + success: false, + message: errorMessage ?? "Saving failed", + }; + } +}; + +const importIntegrationAndModel = async ( + context: { authHeaders: AuthHeaders }, + integrationAndModel: IntegrationAndModel, + existingIntegrations: IntegrationI[], +): Promise => { + const integration = integrationAndModel.integration; + const existingIntegration = existingIntegrations.find( + (i) => i.name === integration.name, + ); + let importIntegrationResult: IntegrationResult; + if (existingIntegration === undefined) { + importIntegrationResult = await importIntegration(context, integration); + if (importIntegrationResult?.success) { + const models = integrationAndModel.models; + const importModelResults: ModelResult[] = await Promise.all( + models.map((model) => importModel(context, model)), + ); + return { + integration: integration, + modelResults: importModelResults, + success: true, + message: "Integration and models imported successfully", + }; + } + } else { + importIntegrationResult = { + integration: existingIntegration, + success: true, + message: "Integration already exists", + }; + + const models = integrationAndModel.models; + const importModelResults: ModelResult[] = await Promise.all( + models.map((model) => importModel(context, model)), + ); + return { + integration: integration, + modelResults: importModelResults, + success: true, + message: "Integration and models imported successfully", + }; + } + + return { + ...importIntegrationResult, + modelResults: [], + }; +}; + +const importPrompt = async ( + context: { authHeaders: AuthHeaders }, + prompt: PromptDef, +): Promise => { + const { authHeaders } = context; + const promptObj = { + models: prompt.integrations, + description: prompt.description, + }; + try { + await fetchWithContext( + `/prompts/${prompt.name}${toMaybeQueryString(promptObj)}`, + {}, + { + method: "POST", + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + body: prompt.template, + }, + ); + return { + prompt, + success: true, + }; + } catch (error: any) { + const errorMessage = await getErrorMessage(error); + return { + prompt, + success: false, + message: errorMessage ?? "Saving failed", + }; + } +}; +export type ImportWorkflowApplicationArgs = { + authHeaders: AuthHeaders; + workflowNames: string[]; + taskNames: string[]; + existingIntegrations: IntegrationI[]; // Merge existing integrations if exists, So that we dont replace their apikey + cardWorkflowDefinitions: WorkflowDef[]; + cardWorkflowDefinitionChanges: WorkflowDef[]; + cardTaskDefinitions?: CommonTaskDef[]; + cardUserForms?: HumanTemplate[]; + cardSchemas?: SchemaDefinition[]; + cardIntegrationsAndModels?: IntegrationAndModel[]; + cardPrompts?: PromptDef[]; +}; + +export type ImportWorkflowApplicationResult = Promise<{ + importWorkflowResults: WorkflowResult[]; + importTaskResults: TaskResult[]; + importUserFormResults: HumanTemplateResult[]; + importSchemaResults: SchemaResult[]; + importIntegrationAndModelResults: IntegrationAndModelResult[]; + importPromptsResult: PromptResult[]; +}>; + +export const importWorkflowWithDependencies = async ( + context: ImportWorkflowApplicationArgs, +): ImportWorkflowApplicationResult => { + const { + workflowNames = [], + taskNames = [], + existingIntegrations = [], + cardWorkflowDefinitions = [], + cardWorkflowDefinitionChanges = [], + cardTaskDefinitions = [], + cardUserForms = [], + cardSchemas = [], + cardIntegrationsAndModels = [], + cardPrompts = [], + } = context; + + // Test if names dont colide between new workflows + const maybeWorkflowRepeatedNames = + new Set(cardWorkflowDefinitionChanges.map(justName)).size !== + cardWorkflowDefinitionChanges.length + ? cardWorkflowDefinitionChanges.map((w) => ({ + workflow: w, + success: false, + message: "Workflows cant have the same name", + })) + : []; + + const maybeTasksRepeatedNames = + new Set(cardTaskDefinitions.map(justName)).size !== + cardTaskDefinitions.length + ? cardTaskDefinitions.map((t) => ({ + task: t, + success: false, + message: "Tasks cant have the same name", + })) + : []; + + if ( + maybeTasksRepeatedNames.length > 0 || + maybeWorkflowRepeatedNames.length > 0 + ) { + return { + importWorkflowResults: maybeWorkflowRepeatedNames, + importTaskResults: maybeTasksRepeatedNames, + importUserFormResults: [], + importSchemaResults: [], + importIntegrationAndModelResults: [], + importPromptsResult: [], + }; + } + + // Re test if the changed dont colide with other already defined workflows + // Patch pre-test for duplicated + const maybeDuplicateWf = cardWorkflowDefinitionChanges.reduce( + (acc, w) => + workflowNames.includes(w.name) + ? acc.concat({ + //@ts-ignore + workflow: w, + success: false, + message: "Workflow already exists", + }) + : acc, + [], + ); + + const maybeDuplicateTasks = cardTaskDefinitions.reduce( + (acc, t) => + taskNames.includes(t.name) + ? acc.concat({ + // @ts-ignore + task: t, + success: false, + message: "Task already exists", + }) + : acc, + [], + ); + + if (maybeDuplicateTasks.length > 0 || maybeDuplicateWf.length > 0) { + return { + importWorkflowResults: maybeDuplicateWf, + importTaskResults: maybeDuplicateTasks, + importUserFormResults: [], + importSchemaResults: [], + importIntegrationAndModelResults: [], + importPromptsResult: [], + }; + } + // Build the replacement operations for workflows + const workflowsChangesOperationsBeforeImport = _zip( + cardWorkflowDefinitions, + cardWorkflowDefinitionChanges, + ).map( + ([ow, cw]) => + (w: Record) => + replaceValues(w, ow!.name, cw!.name), + ); + + // Apply the replacement operations to the workflows + const workflowResults = cardWorkflowDefinitions.map( + _flow(workflowsChangesOperationsBeforeImport), + ); + + /// Integrations handling; + const [ + importWorkflowResults = [], + importTaskResults = [], + importUserFormResults = [], + importSchemaResults = [], + importIntegrationAndModelResults = [], + ] = await Promise.all([ + Promise.all( + workflowResults.map((workflowDefinition: WorkflowDef) => + importWorkflow(context, workflowDefinition), + ), + ), + Promise.all( + cardTaskDefinitions.map((taskDefinition: CommonTaskDef) => + importTask(context, taskDefinition), + ), + ), + Promise.all( + cardUserForms.map((userFormDefinition: HumanTemplate) => + importUserForm(context, userFormDefinition, []), + ), + ), + Promise.all( + cardSchemas.map((schemasDefinition: SchemaDefinition) => + importSchemas(context, schemasDefinition, []), + ), + ), + Promise.all( + cardIntegrationsAndModels.map( + (integrationAndModel: IntegrationAndModel) => + importIntegrationAndModel( + context, + integrationAndModel, + existingIntegrations, + ), + ), + ), + ]); + + // Prompts require integrations to be imported first + const importPromptsResult = await Promise.all( + cardPrompts.map((prompt: PromptDef) => importPrompt(context, prompt)), + ); + // Ask @Gulam why we needed this + // const cacheQueryKey = [fetchContext.stack, WORKFLOW_METADATA_SHORT_URL]; + // queryClient.removeQueries(cacheQueryKey); + + return { + importWorkflowResults, + importTaskResults, + importUserFormResults, + importSchemaResults, + importIntegrationAndModelResults, + importPromptsResult, + }; +}; diff --git a/ui-next/src/utils/constants.ts b/ui-next/src/utils/constants.ts new file mode 100644 index 0000000000..b31b445b73 --- /dev/null +++ b/ui-next/src/utils/constants.ts @@ -0,0 +1,50 @@ +export const ROWS_PER_PAGE_OPTIONS = [ + "15", + "30", + "50", + "100", + "200", + "300", + "500", +] as const; + +export const MS_IN_DAY = 86400000 as const; + +export const DEFAULT_WF_ATTRIBUTES = [ + "workflow.workflowId", + "workflow.output", + "workflow.status", + "workflow.parentWorkflowId", + "workflow.parentWorkflowTaskId", + "workflow.workflowType", + "workflow.version", + "workflow.correlationId", + "workflow.variables", + "workflow.createTime", + "workflow.taskToDomain", +] as const; + +import { editor, type EditorOptions } from "shared/editor"; + +export const SMALL_EDITOR_DEFAULT_OPTIONS: EditorOptions = { + tabSize: 2, + minimap: { enabled: false }, + lightbulb: { enabled: editor.ShowLightbulbIconMode.Off }, + quickSuggestions: true, + lineNumbers: "off", + glyphMargin: false, + folding: false, + // Undocumented see https://github.com/Microsoft/vscode/issues/30795#issuecomment-410998882 + lineDecorationsWidth: 0, + lineNumbersMinChars: 0, + renderLineHighlight: "none", + overviewRulerLanes: 0, + hideCursorInOverviewRuler: true, + scrollbar: { + vertical: "hidden", + // this property is added because it was not allowing us to scroll when mouse pointer is over this component + alwaysConsumeMouseWheel: false, + }, + overviewRulerBorder: false, + automaticLayout: true, +}; diff --git a/ui-next/src/utils/constants/api.ts b/ui-next/src/utils/constants/api.ts new file mode 100644 index 0000000000..11face52c6 --- /dev/null +++ b/ui-next/src/utils/constants/api.ts @@ -0,0 +1,25 @@ +export const WEBHOOK_API_BASE_URL = "/metadata/webhook"; +export const METADATA_MIGRATIONS_ENVIRONMENTS_API_BASE_URL = + "/metadata-migrations/environments"; +export const METADATA_MIGRATIONS_REQUEST_API_BASE_URL = + "/metadata-migrations/requests"; +export const METADATA_MIGRATIONS_API_BASE_URL = "/metadata-migrations"; +export const WORKFLOW_API_BASE_URL = "/workflow"; +export const USER_API_BASE_URL = "/users"; +export const WORKFLOW_METADATA_BASE_URL = `/metadata/workflow`; +export const WORKFLOW_METADATA_BASE_URL_SHORT = + "/metadata/workflow?short=true&metadata=true"; +export const TASK_EXECUTIONS_SEARCH_URL = "/tasks/search?"; + +const INTEGRATIONS_BASE = "/integrations"; +export const INTEGRATIONS_API_URL = { + BASE: INTEGRATIONS_BASE, + ALL: `${INTEGRATIONS_BASE}/all`, + DEF: `${INTEGRATIONS_BASE}/def`, + EVENT_STATS: `${INTEGRATIONS_BASE}/eventStats`, + PROVIDER: `${INTEGRATIONS_BASE}/provider`, +}; + +export const WORKFLOW_METADATA_SHORT_URL = + "/metadata/workflow?short=true&metadata=true"; +export const ROLES_API_BASE_URL = "/roles"; diff --git a/ui-next/src/utils/constants/common.ts b/ui-next/src/utils/constants/common.ts new file mode 100644 index 0000000000..aa2a156cc0 --- /dev/null +++ b/ui-next/src/utils/constants/common.ts @@ -0,0 +1,115 @@ +import { HTTPMethods } from "types/TaskType"; + +export const LOCAL_STORAGE_KEY = { + ROWS_PER_PAGE: "rowsPerPage", +}; + +export const FORBIDDEN_DELETE_ERROR_MESSAGE = + "Deletion failed, it looks like you do not have permissions to delete this resource."; + +export const FORBIDDEN_PUT_ERROR_MESSAGE = + "Update failed, it looks like you do not have permissions to update this resource."; + +export const FORBIDDEN_POST_ERROR_MESSAGE = + "Creation failed, it looks like you do not have permissions to create this resource."; + +export const FORBIDDEN_GET_ERROR_MESSAGE = + "It looks like you do not have permissions to get this resource."; + +export const generateForbiddenMessage = (method: HTTPMethods) => { + switch (method) { + case HTTPMethods.POST: + return FORBIDDEN_POST_ERROR_MESSAGE; + case HTTPMethods.PUT: + case HTTPMethods.PATCH: + return FORBIDDEN_PUT_ERROR_MESSAGE; + case HTTPMethods.DELETE: + return FORBIDDEN_DELETE_ERROR_MESSAGE; + default: + return FORBIDDEN_GET_ERROR_MESSAGE; + } +}; + +/** + * output: Feb 21, 2023 12:19 AM + */ +export const FORMAT_TIME_TO_LONG = "MMM d, yyyy hh:mm a"; + +/** + * output: 2023-11-16 12:00 AM + */ +export const FORMAT_DATE_TIME_PICKER = "yyyy-MM-dd hh:mm aa"; + +export const SEARCH_QUERY_PARAM = "search"; +export const PAGE_QUERY_PARAM = "page"; +export const FILTER_QUERY_PARAM = "filter"; +export const ACTIVE_FILTER_QUERY_PARAM = "activeFilter"; +export const USER_ROLE_FILTER_QUERY_PARAM = "roleFilter"; + +export const HTTP_TEST_ENDPOINT = + "https://orkes-api-tester.orkesconductor.com/api"; + +export const HOT_KEYS_SIDEBAR = "sidebar"; +export const HOT_KEYS_WORKFLOW_DEFINITION = "workflow-definition"; + +export const TITLE_ALLOWED_CHARS = "^(?!-)[a-zA-Z0-9_-]*$"; + +export const ALPHANUMERIC_UNDERSCORE_HYPHEN_PATTERN = "^[a-zA-Z0-9_-]*$"; +export const WORKFLOW_NAME_ERROR_MESSAGE = + "The name should contain only letters (both uppercase and lowercase), digits, spaces, and the characters <, >, {, }, #, and -. No other special characters are allowed."; + +export const TASK_NAME_ERROR_MESSAGE = + "The name should contain only letters (both uppercase and lowercase), digits, spaces, and the characters <, >, {, }, #, and -. No other special characters are allowed."; + +export const HEADER_Z_INDEX = 1000; + +export const WORKFLOW_SEARCH_QUERY_SUGGESTIONS = [ + "createTime", + "updateTime", + "createdBy", + "updatedBy", + "status", + "endTime", + "workflowId", + "parentWorkflowId", + "parentWorkflowTaskId", + "output", + "taskToDomain", + "priority", + "variables", + "lastRetriedTime", + "history", + "idempotencyKey", + "rateLimited", + "startTime", + "workflowName", + "workflowVersion", + "correlationId", +]; + +export const TASK_SEARCH_QUERY_SUGGESTIONS = [ + "taskType", + "referenceTaskName", + "retryCount", + "seq", + "pollCount", + "taskDefName", + "startDelayInSeconds", + "retried", + "executed", + "callbackFromWorker", + "responseTimeoutSeconds", + "workflowInstanceId", + "workflowType", + "taskId", + "callbackAfterSeconds", + "workerId", + "outputData", +]; + +export const enum ButtonPosition { + TOP = "top", + RIGHT = "right", + BOTTOM = "bottom", + LEFT = "left", +} diff --git a/ui-next/src/utils/constants/dateTimePicker.ts b/ui-next/src/utils/constants/dateTimePicker.ts new file mode 100644 index 0000000000..b9541aea13 --- /dev/null +++ b/ui-next/src/utils/constants/dateTimePicker.ts @@ -0,0 +1,65 @@ +export const COMMONLY_USED = { + today: { + name: "Today", + description: "Started today at 00:00 and ended before today at 23:59:59", + }, + last15Minutes: { + name: "Last 15 minutes", + description: "Started 15 minutes ago and ended before now", + }, + yesterday: { + name: "Yesterday", + description: + "Started yesterday at 00:00 and ended before yesterday at 23:59:59", + }, + last30Minutes: { + name: "Last 30 minutes", + description: "Started 30 minutes ago and ended before now", + }, + last48Hours: { + name: "Last 48 hours", + description: "Started 48 hours ago and ended before now", + }, + last1Hour: { + name: "Last 1 hour", + description: "Started 1 hour ago and ended before now", + }, + last72Hours: { + name: "Last 72 hours", + description: "Started 72 hours ago and ended before now", + }, + last4Hours: { + name: "Last 4 hours", + description: "Started 4 hours ago and ended before now", + }, + lastWeek: { + name: "Last week", + description: "Started 7 days ago and ended before now", + }, + last12Hours: { + name: "Last 12 hours", + description: "Started 12 hours ago and ended before now", + }, +}; + +export const TIME_FRAMES = { + last: "Last", + next: "Next", +}; + +export const COUNT_OPTIONS = ["1", "5", "10", "15", "30", "45"]; + +export const TIME_OPTIONS = { + seconds: "Seconds", + minutes: "Minutes", + hours: "Hours", + days: "Days", + weeks: "Weeks", + months: "Months", +}; + +export const REFRESH_TIME_OPTIONS = { + seconds: "Seconds", + minutes: "Minutes", + hours: "Hours", +}; diff --git a/ui-next/src/utils/constants/docLink.ts b/ui-next/src/utils/constants/docLink.ts new file mode 100644 index 0000000000..1bb56eaf6a --- /dev/null +++ b/ui-next/src/utils/constants/docLink.ts @@ -0,0 +1,17 @@ +export const DOC_LINK_URL = { + TASK_DEFINITION: + "https://orkes.io/content/developer-guides/tasks#task-definition", + HUMAN_TASK_USER_FORM: + "https://orkes.io/content/developer-guides/orchestrating-human-tasks#creating-user-forms", + HUMAN_TASK_SEARCH: + "https://orkes.io/content/reference-docs/api/human-tasks/search-task-list#request-body", + WEBHOOKS: "https://orkes.io/content/developer-guides/webhook-integration", + AI_PROMPTS: + "https://orkes.io/content/developer-guides/creating-and-managing-gen-ai-prompt-templates", + EVENT_HANDLER: "https://orkes.io/content/developer-guides/event-handler", + SECRETS: "https://orkes.io/content/developer-guides/secrets-in-conductor", + SCHEDULER: "https://orkes.io/content/developer-guides/scheduling-workflows", + ENV_VARIABLES: + "https://orkes.io/content/developer-guides/using-environment-variables", + REMOTE_SERVICES: "https://orkes.io/content/remote-services", +}; diff --git a/ui-next/src/utils/constants/emailContentTypeSuggestions.ts b/ui-next/src/utils/constants/emailContentTypeSuggestions.ts new file mode 100644 index 0000000000..8dd543227d --- /dev/null +++ b/ui-next/src/utils/constants/emailContentTypeSuggestions.ts @@ -0,0 +1 @@ +export const EMAIL_CONTENT_TYPE_SUGGESTIONS = ["text/plain", "text/html"]; diff --git a/ui-next/src/utils/constants/event.ts b/ui-next/src/utils/constants/event.ts new file mode 100644 index 0000000000..ed8a088da7 --- /dev/null +++ b/ui-next/src/utils/constants/event.ts @@ -0,0 +1 @@ +export const MESSAGE_BROKER = "MESSAGE_BROKER"; diff --git a/ui-next/src/utils/constants/httpStatusCode.ts b/ui-next/src/utils/constants/httpStatusCode.ts new file mode 100644 index 0000000000..58fc23d1ba --- /dev/null +++ b/ui-next/src/utils/constants/httpStatusCode.ts @@ -0,0 +1,73 @@ +export enum HttpStatusCode { + // 1xx Informational + Continue = 100, + SwitchingProtocols = 101, + Processing = 102, + + // 2xx Success + OK = 200, + Created = 201, + Accepted = 202, + NonAuthoritativeInformation = 203, + NoContent = 204, + ResetContent = 205, + PartialContent = 206, + MultiStatus = 207, + AlreadyReported = 208, + IMUsed = 226, + + // 3xx Redirection + MultipleChoices = 300, + MovedPermanently = 301, + Found = 302, + SeeOther = 303, + NotModified = 304, + UseProxy = 305, + Unused = 306, + TemporaryRedirect = 307, + PermanentRedirect = 308, + + // 4xx Client Error + BadRequest = 400, + Unauthorized = 401, + PaymentRequired = 402, + Forbidden = 403, + NotFound = 404, + MethodNotAllowed = 405, + NotAcceptable = 406, + ProxyAuthenticationRequired = 407, + RequestTimeout = 408, + Conflict = 409, + Gone = 410, + LengthRequired = 411, + PreconditionFailed = 412, + PayloadTooLarge = 413, + URITooLong = 414, + UnsupportedMediaType = 415, + RangeNotSatisfiable = 416, + ExpectationFailed = 417, + ImATeapot = 418, + MisdirectedRequest = 421, + UnprocessableEntity = 422, + Locked = 423, + FailedDependency = 424, + TooEarly = 425, + UpgradeRequired = 426, + PreconditionRequired = 428, + TooManyRequests = 429, + RequestHeaderFieldsTooLarge = 431, + UnavailableForLegalReasons = 451, + + // 5xx Server Error + InternalServerError = 500, + NotImplemented = 501, + BadGateway = 502, + ServiceUnavailable = 503, + GatewayTimeout = 504, + HTTPVersionNotSupported = 505, + VariantAlsoNegotiates = 506, + InsufficientStorage = 507, + LoopDetected = 508, + NotExtended = 510, + NetworkAuthenticationRequired = 511, +} diff --git a/ui-next/src/utils/constants/httpSuggestions.ts b/ui-next/src/utils/constants/httpSuggestions.ts new file mode 100644 index 0000000000..c90a156024 --- /dev/null +++ b/ui-next/src/utils/constants/httpSuggestions.ts @@ -0,0 +1,79 @@ +export const HEADER_SUGGESTIONS = [ + /* "Accept", */ + "Accept-Language", + "Authorization", + "Cache-Control", + "Content-MD5", + /* "Content-Type", */ + "From", + "If-Match", + "If-Modified-Since", + "If-None-Match", + "Max-Forwards", + "Pragma", + "If-Range", + "If-Unmodified-Since", + "Proxy-Authorization", + "Range", + "Warning", + "x-api-key", + "Accept-Charset", + "Accept-Encoding", + "Accept-Control-Request-Headers", + "Accept-Control-Request-Method", + "Content-Transfer-Encoding", + "Expect", + "Transfer-Encoding", + "Trailer", +]; + +export const CONTENT_TYPE_SUGGESTIONS = [ + "application/java-archive", + "application/EDI-X12", + "application/EDIFACT", + "application/javascript", + "application/octet-stream", + "application/ogg", + "application/pdf", + "application/xhtml+xml", + "application/x-shockwave-flash", + "application/json", + "application/ld+json", + "application/xml", + "application/zip", + "application/x-www-form-urlencoded", + "audio/mpeg", + "audio/x-ms-wma", + "audio/vnd.rn-realaudio", + "audio/x-wav", + "image/gif", + "image/jpeg", + "image/png", + "image/tiff", + "image/vnd.microsoft.icon", + "image/x-icon", + "image/vnd.djvu", + "image/svg+xml", +]; + +export const MEDIA_TYPE_SUGGESTIONS = [ + "application/pdf", + "text/html", + "text/plain", + "application/json", +]; + +export const ACCEPT_PATH = "accept"; +export const HEADERS_PATH = "headers"; +export const CONTENT_TYPE_PATH = "contentType"; +export const METHOD_PATH = "method"; +export const HEDGING_CONFIG_PATH = "hedgingConfig"; +export const SERVICE_PATH = "service"; +export const HTTP_REQUEST_PATH = "inputParameters.http_request"; +export const POLLING_STRATEGY_PATH = "pollingStrategy"; +export const WAIT_UNTIL_PATH = "inputParameters.wait.inputParameters.until"; +export const WAIT_DURATION_PATH = + "inputParameters.wait.inputParameters.duration"; +export const URI_PATH = "uri"; +export const HTTP_REQUEST_BODY = "body"; +export const HTTP_REQUEST_ENCODE = "encode"; diff --git a/ui-next/src/utils/constants/index.ts b/ui-next/src/utils/constants/index.ts new file mode 100644 index 0000000000..6bcf8f16a5 --- /dev/null +++ b/ui-next/src/utils/constants/index.ts @@ -0,0 +1 @@ +export * from "./event"; diff --git a/ui-next/src/utils/constants/jsonSchema.ts b/ui-next/src/utils/constants/jsonSchema.ts new file mode 100644 index 0000000000..0835c65916 --- /dev/null +++ b/ui-next/src/utils/constants/jsonSchema.ts @@ -0,0 +1,2 @@ +export const JSON_SCHEMA_DRAFT_07_URL = + "http://json-schema.org/draft-07/schema"; diff --git a/ui-next/src/utils/constants/regex.ts b/ui-next/src/utils/constants/regex.ts new file mode 100644 index 0000000000..ab4f57902d --- /dev/null +++ b/ui-next/src/utils/constants/regex.ts @@ -0,0 +1,17 @@ +export const CONTAIN_VARIABLE_SYNTAX_REGEX = /^(?=.*?[${}]{1}).*$/; + +// The backend allows everything but a semicolon +// https://github.com/orkes-io/conductor/blob/f07dc36f08dcaf91cb40ea6ee211c840de5ac8f3/common/src/main/java/com/netflix/conductor/common/metadata/workflow/WorkflowDefSummary.java#L29C24-L29C89 +// Using `()` would cause errors in querys such as: +// workflowType IN (wf_name(test), wf_name2), because the +// end parenthesis would be interpreted as the end of the +// IN clause. +export const WORKFLOW_NAME_REGEX = + /^(?! )[.A-Za-z0-9!@#$%^&*_<>{}[\]|+=\s-]+(?{}#\s-]*$/; + +// toString() here will escape some chars, +// the slice() will remove the trailing "/" from both ends +export const regexToString = (regex: RegExp): string => + regex.toString().slice(1, -1); diff --git a/ui-next/src/utils/constants/route.ts b/ui-next/src/utils/constants/route.ts new file mode 100644 index 0000000000..514f08b8ef --- /dev/null +++ b/ui-next/src/utils/constants/route.ts @@ -0,0 +1,168 @@ +export const GET_STARTED_URL = "/get-started"; +export const HUB_URL = "/hub"; +export const WEBHOOK_ROUTE_URL = { + LIST: "/configure-webhooks", + ID: "/configure-webhooks/:id", + NEW: "/newWebhook", +}; + +export const METADATA_MIGRATION_ENVIRONMENT_URL = { + BASE: "/environment", + LIST: "/environments", + ID: "/environments/:id", + NEW: "/newEnvironment", +}; + +export const METADATA_MIGRATION_REQUEST_URL = { + BASE: "/migrationRequest", +}; + +export const NEW_TASK_DEF_URL = "/newTaskDef"; +export const TASK_DEF_URL = { + BASE: "/taskDef", + NAME: "/taskDef/:name", +}; + +export const WORKFLOW_DEFINITION_URL = { + BASE: "/workflowDef", + NAME_VERSION: "/workflowDef/:name/:version?", + NEW: "/newWorkflowDef", +}; + +export const SCHEDULER_DEFINITION_URL = { + BASE: "/scheduleDef", + NAME: "/scheduleDef/:name?", + NEW: "/newScheduleDef", +}; + +export const SCHEDULER_EXECUTION_URL = "/schedulerExecs"; + +export const USER_MANAGEMENT_URL = { + BASE: "/userManagement", + TYPE_ID: "/userManagement/:type?/:id?", + LIST: "/userManagement/users", + EDIT: "/userManagement/users/:id", +}; + +export const INTEGRATIONS_MANAGEMENT_URL = { + BASE: "/integrations", + ADD: "/integrations/addIntegration", + EDIT: "/integrations/:id/integration", + EDIT_INTEGRATION_MODEL: "/integrations/:id/configuration", + EDIT_AI_MODEL: "/integrations/:id?/integration/:aiModelId", +}; + +export const AI_PROMPTS_MANAGEMENT_URL = { + BASE: "/ai_prompts", + NEW_AI_PROMPT_MODEL: "/ai_prompts/new_ai_prompt_model", + EDIT: "/ai_prompts/:id/:version?", +}; + +export const GROUP_MANAGEMENT_URL = { + BASE: "/groupManagement", + TYPE_ID: "/groupManagement/:type?/:id?", + LIST: "/groupManagement/groups", + EDIT: "/groupManagement/groups/:id", +}; + +export const APPLICATION_MANAGEMENT_URL = { + BASE: "/applicationManagement", + TYPE_ID: "/applicationManagement/:type?/:id?", + LIST: "/applicationManagement/applications", + EDIT: "/applicationManagement/applications/:id", +}; + +export const ROLE_MANAGEMENT_URL = { + BASE: "/roleManagement", + TYPE_ID: "/roleManagement/:type?/:id?", + LIST: "/roleManagement/roles", + EDIT: "/roleManagement/roles/:id", +}; + +export const EVENT_HANDLERS_URL = { + BASE: "/eventHandlerDef", + NAME: "/eventHandlerDef/:name", + NEW: "/newEventHandlerDef", +}; + +export const TASK_QUEUE_URL = { + BASE: "/taskQueue", + NAME: "/taskQueue/:name?", +}; + +export const SECRETS_URL = { + BASE: "/secrets", +}; + +export const RUN_WORKFLOW_URL = "/runWorkflow"; + +export const HUMAN_TASK_URL = { + BASE: "/human", + LIST: "/human/tasks", + TEMPLATES: "/human/templates", + TEMPLATES_NAME_VERSION: "/human/templates/:templateName/:version?", + TASK: "/human/task", + TASK_ID: "/human/task/:taskId?", + TASK_INBOX: "/human/task-inbox", +}; + +export const SCHEMAS_URL = { + BASE: "/schemas", + EDIT: "/schemas/:schemaName/:version?", + DEF: "/schemas/schemaDef", +}; + +export const REMOTE_SERVICES_URL = { + BASE: "/remote-services", + EDIT: "/remote-services/:serviceName", + NEW: "/newRemoteServiceDef", +}; + +export const SERVICE_URL = { + LIST: "/services", + EDIT: "/services/edit/:serviceId", + NEW: "/newService", + SERVICE_ID: "/services/:serviceId", + SWAGGER: "/services/:serviceId/swagger", + ROUTE_DETAILS: "/services/:serviceId/routes/*", + ROUTE_EDIT: "/services/:serviceId/edit/routes/*", + NEW_ROUTE: "/services/:serviceId/routes/new", +}; + +export const AUTHENTICATION_URL = "/authentication"; + +export const ERROR_URL = "/error"; + +export const WORKFLOW_EXPLORER_URL = "/workflowExplorer"; + +export const OIDC_CALLBACK_ROUTE = "/login/oidc/callback"; + +export const WORKFLOW_EXECUTION_URL = { + BASE: "/execution", + WF_ID_TASK_ID: "/execution/:id/:taskId?", +}; + +export const TASK_EXECUTION_URL = { + LIST: "/taskExecs", +}; + +export const ENV_VARIABLES_URL = { + BASE: "/environment", +}; + +export const EVENT_MONITOR_URL = { + BASE: "/eventMonitor", + NAME: "/eventMonitor/:name", +}; + +export const WORKERS_URL = { + BASE: "/workers", +}; + +export const TAGS_DASHBOARD_URL = { + BASE: "/tags-dashboard", +}; + +export const API_REFERENCE_URL = { + BASE: "/api-reference", +}; diff --git a/ui-next/src/utils/constants/switch.ts b/ui-next/src/utils/constants/switch.ts new file mode 100644 index 0000000000..0758d6955b --- /dev/null +++ b/ui-next/src/utils/constants/switch.ts @@ -0,0 +1 @@ +export const SWITCH_CASE_PREFIX = "switch_case"; diff --git a/ui-next/src/utils/constants/task.ts b/ui-next/src/utils/constants/task.ts new file mode 100644 index 0000000000..e513b439fe --- /dev/null +++ b/ui-next/src/utils/constants/task.ts @@ -0,0 +1,12 @@ +export const TASK_STATUS = { + IN_PROGRESS: "IN_PROGRESS", + CANCELED: "CANCELED", + FAILED: "FAILED", + FAILED_WITH_TERMINAL_ERROR: "FAILED_WITH_TERMINAL_ERROR", + COMPLETED: "COMPLETED", + COMPLETED_WITH_ERRORS: "COMPLETED_WITH_ERRORS", + SCHEDULED: "SCHEDULED", + TIMED_OUT: "TIMED_OUT", + SKIPPED: "SKIPPED", + PENDING: "PENDING", +}; diff --git a/ui-next/src/utils/constants/webhook.ts b/ui-next/src/utils/constants/webhook.ts new file mode 100644 index 0000000000..1d9cb9139d --- /dev/null +++ b/ui-next/src/utils/constants/webhook.ts @@ -0,0 +1,155 @@ +import { TagDto } from "types/Tag"; + +export enum SOURCE_PLATFORM { + GITHUB = "Github", + MICROSOFT_TEAMS = "Microsoft Teams", + SEND_GRID = "SendGrid", + SLACK = "Slack", + STRIPE = "Stripe", + CUSTOM = "Custom", +} + +export enum VERIFIER { + SLACK_BASED = "SLACK_BASED", + SIGNATURE_BASED = "SIGNATURE_BASED", + HEADER_BASED = "HEADER_BASED", + HMAC_BASED = "HMAC_BASED", + STRIPE = "STRIPE", + SEND_GRID = "SENDGRID", +} + +export const WEBHOOK_HEADER_NAME = { + STRIPE_SIGNATURE: "Stripe-Signature", + X_HUB_SIGNATURE_256: "X-Hub-Signature-256", + AUTHORIZATION: "Authorization", +}; + +export type WebhookAuthParam = { + vendor: SOURCE_PLATFORM; + signing: string; + headerKey?: string; + secretKey?: string; + secretKeyLabel?: string; + secretValue?: string; + secretLabel?: string; + iconName: string; +}; + +export const WEBHOOK_ICON = { + [SOURCE_PLATFORM.SLACK]: "slack-icon", + [SOURCE_PLATFORM.GITHUB]: "github-icon", + [SOURCE_PLATFORM.STRIPE]: "stripe-icon", + [SOURCE_PLATFORM.SEND_GRID]: "send-grid-icon", + [SOURCE_PLATFORM.MICROSOFT_TEAMS]: "microsoft-teams-icon", + [SOURCE_PLATFORM.CUSTOM]: "default-icon", +}; + +export const WEBHOOK_AUTH_PARAMS: WebhookAuthParam[] = [ + { + vendor: SOURCE_PLATFORM.SLACK, + signing: VERIFIER.SLACK_BASED, + iconName: WEBHOOK_ICON[SOURCE_PLATFORM.SLACK], + }, + { + vendor: SOURCE_PLATFORM.GITHUB, + signing: VERIFIER.SIGNATURE_BASED, + headerKey: WEBHOOK_HEADER_NAME.X_HUB_SIGNATURE_256, + secretLabel: "Secret", + iconName: WEBHOOK_ICON[SOURCE_PLATFORM.GITHUB], + }, + { + vendor: SOURCE_PLATFORM.STRIPE, + signing: VERIFIER.STRIPE, + headerKey: WEBHOOK_HEADER_NAME.STRIPE_SIGNATURE, + secretValue: "endpointSecret", + secretLabel: "Endpoint secret", + iconName: WEBHOOK_ICON[SOURCE_PLATFORM.STRIPE], + }, + { + vendor: SOURCE_PLATFORM.SEND_GRID, + signing: VERIFIER.SEND_GRID, + secretKeyLabel: "Verification key", + iconName: WEBHOOK_ICON[SOURCE_PLATFORM.SEND_GRID], + }, + { + vendor: SOURCE_PLATFORM.MICROSOFT_TEAMS, + signing: VERIFIER.HMAC_BASED, + headerKey: WEBHOOK_HEADER_NAME.AUTHORIZATION, + secretLabel: "Security token", + iconName: WEBHOOK_ICON[SOURCE_PLATFORM.MICROSOFT_TEAMS], + }, + { + vendor: SOURCE_PLATFORM.CUSTOM, + signing: VERIFIER.HEADER_BASED, + iconName: WEBHOOK_ICON[SOURCE_PLATFORM.CUSTOM], + }, +]; + +export interface IWebhookDTO { + id?: string; + name: string; + receiverWorkflowNamesToVersions: { [key: string]: number }; + authenticationType: string; + urlVerified?: boolean; + sourcePlatform: string; + headers?: { [key: string]: string }; + bodyKey?: string; + bodyValue?: string; + headerKey?: string; + secretKey?: string; + secretValue?: string; + verifier?: VERIFIER; + workflowsToStart?: { [key: string]: number | string }; + url?: string; + tags?: TagDto[]; +} + +export interface WebhookHistoryDTO { + eventId?: string; + matched?: boolean; + workflowIds?: string[]; + timeStamp?: number; +} + +export enum REPEATER_KEY { + HEADERS = "headers", +} + +export const GUIDE_STEPS = [ + { + id: "webhookName", + title: "Webhook Name", + description: "name for the webhook.", + }, + { + id: "webhookEvent", + title: "Workflows to receive webhook event", + description: "Workflows that are supposed to receive this webhook event.", + }, + { + id: "sourcePlatform", + title: "Source Platform", + description: "Platform from which this webhook event will be invoked.", + }, + { + id: "url", + title: "URL", + description: "URL on which the webhook event must be invoked.", + }, + { + id: "urlStatus", + title: "URL Status", + description: + "Unverified - No single event has been received on the URL. \nVerified - Either url verification is done or successful event has been received.", + }, + { + id: "startWf", + title: "Start workflow when webhook comes", + description: "Start a new workflow when the webhook event comes.", + }, + { + id: "secret", + title: "Secret", + description: "Secret will be visible only once while storing.", + }, +]; diff --git a/ui-next/src/utils/constants/workflow.ts b/ui-next/src/utils/constants/workflow.ts new file mode 100644 index 0000000000..80c731f43f --- /dev/null +++ b/ui-next/src/utils/constants/workflow.ts @@ -0,0 +1,30 @@ +export const WORKFLOW_STATUS = { + RUNNING: "RUNNING", + COMPLETED: "COMPLETED", + FAILED: "FAILED", + TIMED_OUT: "TIMED_OUT", + TERMINATED: "TERMINATED", + PAUSED: "PAUSED", +}; + +export const ZOOMING_STEP = 0.1; + +export const TEST_TASK_TYPES = [ + "HTTP", + "HTTP_POLL", + "INLINE", + "DO_WHILE", + "DYNAMIC", + "UPDATE_SECRET", + "SWITCH", + "QUERY_PROCESSOR", + "JSON_JQ_TRANSFORM", + "LLM_TEXT_COMPLETE", + "LLM_GENERATE_EMBEDDINGS", + "LLM_GET_EMBEDDINGS", + "LLM_STORE_EMBEDDINGS", + "LLM_SEARCH_INDEX", + "LLM_INDEX_DOCUMENT", + "LLM_INDEX_TEXT", + "GET_WORKFLOW", +]; diff --git a/ui-next/src/utils/constants/workflowScheduleExecution.ts b/ui-next/src/utils/constants/workflowScheduleExecution.ts new file mode 100644 index 0000000000..f631830fe2 --- /dev/null +++ b/ui-next/src/utils/constants/workflowScheduleExecution.ts @@ -0,0 +1,5 @@ +export const WORKFLOW_SCHEDULE_EXECUTION_STATE = { + POLLED: "POLLED", + FAILED: "FAILED", + EXECUTED: "EXECUTED", +}; diff --git a/ui-next/src/utils/cronHelpers.ts b/ui-next/src/utils/cronHelpers.ts new file mode 100644 index 0000000000..3d669c97e0 --- /dev/null +++ b/ui-next/src/utils/cronHelpers.ts @@ -0,0 +1,41 @@ +import cron from "cron-validate"; + +export const cronExpressionIsValid = ( + cronExpression: string, +): { isValid: boolean; errors: any } => { + try { + const cronResult = cron(cronExpression, { + preset: "default", + override: { + // seconds field + useSeconds: true, + // the ? alias + useBlankDay: true, + // aliases like 'mon' + useAliases: true, + // allow 'L' for last day of month + useLastDayOfMonth: true, + // allow 'W' for last day of week + useLastDayOfWeek: true, + useNearestWeekday: true, + useNthWeekdayOfMonth: true, + }, + }); + if (cronResult.isValid()) { + return { + isValid: true, + errors: null, + }; + } else { + return { + isValid: false, + errors: cronResult.getError(), + }; + } + } catch (e: any) { + return { + isValid: false, + errors: [e.message], + }; + } +}; diff --git a/ui-next/src/utils/date.ts b/ui-next/src/utils/date.ts new file mode 100644 index 0000000000..6a494472e3 --- /dev/null +++ b/ui-next/src/utils/date.ts @@ -0,0 +1,637 @@ +import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns"; +import { + addMinutes, + differenceInSeconds, + Duration, + endOfDay, + format, + formatDistance, + formatDistanceToNow, + fromUnixTime, + intervalToDuration, + isValid, + parseISO, + setMilliseconds, + setMinutes, + setSeconds, + startOfDay, + subDays, + subHours, + subMinutes, +} from "date-fns"; +import { + format as formatTz, + formatInTimeZone as formatInTz, + toDate, + utcToZonedTime, + zonedTimeToUtc, +} from "date-fns-tz"; +import { isNil as _isNil } from "lodash"; +import _isEmpty from "lodash/isEmpty"; + +export function durationRenderer(durationMs: number) { + const duration: Duration = intervalToDuration({ start: 0, end: durationMs }); + if (durationMs > 5000) { + if (duration?.months != null && duration.months > 0) { + return `${duration.months} months ${duration.days}d ${duration.hours}h ${duration.minutes}m ${duration.seconds}s`; + } + if (duration?.days != null && duration?.days > 0) { + return `${duration.days}d ${duration.hours}h ${duration.minutes}m ${duration.seconds}s`; + } else if (duration?.hours != null && duration.hours > 0) { + return `${duration.hours}h ${duration.minutes}m ${duration.seconds}s`; + } else { + return `${duration.minutes}m ${duration.seconds}s`; + } + } else { + return `${durationMs}ms`; + } + + //return !isNaN(durationMs) && (durationMs > 0? formatDuration({seconds: durationMs/1000}): '0.0 seconds'); +} + +export function timestampRenderer(date?: number | string) { + return !_isNil(date) && Number(date) !== 0 + ? format(new Date(date), "yyyy-MM-dd HH:mm:ss") + : ""; // could be string or number. +} + +export function timestampRendererLocal(date?: number | string) { + if (!_isNil(date) && Number(date) !== 0) { + try { + const newDate = new Date(date); + return format(newDate, "yyyy-MM-dd'T'HH:mm"); + } catch { + return ""; + } + } else { + return ""; + } +} + +// Functions exported directly from date-fns +export { addMinutes, differenceInDays, parse } from "date-fns"; + +const DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; +export const DATE_FORMAT = "yyyy-MM-dd HH:mm"; + +export const EXPECTED_DATE_FORMAT = "yyyy-MM-dd hh:mm a"; + +export const DateAdapter = AdapterDateFns; + +export const getDateTime = ( + timeframe: string, + count: string, + unit: string, + roundToMinute = false, +) => { + const now = new Date(); + const result = new Date(now); + + switch (unit) { + case "seconds": + if (timeframe === "last") { + result.setSeconds(result.getSeconds() - Number(count)); + } else { + result.setSeconds(result.getSeconds() + Number(count)); + } + break; + case "minutes": + if (timeframe === "last") { + result.setMinutes(result.getMinutes() - Number(count)); + } else { + result.setMinutes(result.getMinutes() + Number(count)); + } + break; + case "hours": + if (timeframe === "last") { + result.setHours(result.getHours() - Number(count)); + } else { + result.setHours(result.getHours() + Number(count)); + } + break; + case "days": + if (timeframe === "last") { + result.setDate(result.getDate() - Number(count)); + } else { + result.setDate(result.getDate() + Number(count)); + } + break; + case "weeks": + if (timeframe === "last") { + result.setDate(result.getDate() - Number(count) * 7); + } else { + result.setDate(result.getDate() + Number(count) * 7); + } + break; + + default: + if (timeframe === "last") { + result.setMonth(result.getMonth() - Number(count)); + } else { + result.setMonth(result.getMonth() + Number(count)); + } + break; + } + + if (roundToMinute) { + result.setSeconds(0); + result.setMilliseconds(0); + } + + return result.toString(); +}; + +export const commonlyUsedDateTime = (timeKey: string) => { + const now = new Date(); + const time = new Date(now); + switch (timeKey) { + case "yesterday": + time.setDate(time.getDate() - 1); + return { + rangeStart: time.setHours(0, 0, 0, 0).toString(), + rangeEnd: time.setHours(23, 59, 59, 999).toString(), + name: "Yesterday", + }; + case "last15Minutes": + return { + rangeStart: time.setMinutes(time.getMinutes() - 15).toString(), + rangeEnd: "", + name: "Last 15 Minutes", + }; + case "last30Minutes": + return { + rangeStart: time.setMinutes(time.getMinutes() - 30).toString(), + rangeEnd: "", + name: "Last 30 Minutes", + }; + case "last48Hours": + return { + rangeStart: time.setHours(time.getHours() - 48).toString(), + name: "Last 48 Hours", + rangeEnd: "", + }; + case "last1Hour": + return { + rangeStart: time.setHours(time.getHours() - 1).toString(), + name: "Last 1 Hour", + rangeEnd: "", + }; + case "last72Hours": + return { + rangeStart: time.setHours(time.getHours() - 72).toString(), + name: "Last 72 Hours", + rangeEnd: "", + }; + case "last4Hours": + return { + rangeStart: time.setHours(time.getHours() - 4).toString(), + name: "Last 4 Hours", + rangeEnd: "", + }; + case "lastWeek": + return { + rangeStart: time.setDate(time.getDate() - 7).toString(), + name: "Last Week", + rangeEnd: "", + }; + case "last12Hours": + return { + rangeStart: time.setHours(time.getHours() - 12).toString(), + name: "Last 12 Hours", + rangeEnd: "", + }; + + default: + return { + rangeStart: time.setHours(0, 0, 0, 0).toString(), + name: "Today", + rangeEnd: time.setHours(23, 59, 59, 999).toString(), + }; + } +}; + +export const getSearchDateTime = (start: string, end: string) => { + const formattedStartDate = new Date(Number(start)).toLocaleDateString( + "en-US", + { month: "short", day: "numeric", year: "numeric" }, + ); + const formattedEndDate = new Date(Number(end)).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }); + const startTime = new Date(Number(start)).toLocaleTimeString("en-US", { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hourCycle: "h23", + }); + const endTime = new Date(Number(end)).toLocaleTimeString("en-US", { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hourCycle: "h23", + }); + + if (!start) { + return `${formattedEndDate} @ ${endTime}`; + } else if (!end) { + return `${formattedStartDate} @ ${startTime}`; + } else { + return `${formattedStartDate} @ ${startTime} - ${formattedEndDate} @ ${endTime}`; + } +}; + +export const formatDateTo24Hrs = (dateString: string) => { + return format(new Date(Number(dateString)), DATE_TIME_FORMAT); +}; + +export const getRefreshRate = (count: string, type: string) => { + switch (type) { + case "minutes": + return `${Number(count) * 60 * 1000}`; + case "hours": + return `${Number(count) * 60 * 60 * 1000}`; + default: + return `${Number(count) * 1000}`; + } +}; + +export const getCombineDateTime = (date: string, time: string) => { + // Extracting date components from date + const startDate = new Date(Number(date)); + const year = startDate.getFullYear(); + const month = startDate.getMonth(); + const day = startDate.getDate(); + + // Extracting time components from time + const startTime = new Date(Number(time)); + const hours = startTime.getHours(); + const minutes = startTime.getMinutes(); + const seconds = startTime.getSeconds(); + + // Creating a new Date object with combined date and time + const combinedDateTime = new Date(year, month, day, hours, minutes, seconds); + + return combinedDateTime.getTime().toString(); +}; + +export const printableUpdatedTime = (updatedTimeInMillis?: number): string => { + if (updatedTimeInMillis == null || updatedTimeInMillis === 0) { + return "0 minutes ago"; + } + const printableUpdatedTime = formatDistanceToNow( + new Date(updatedTimeInMillis), + { + addSuffix: true, + }, + ); + return printableUpdatedTime; +}; + +export const maybeFormatDate = (dateString: string): string => { + if (_isEmpty(dateString)) { + return dateString; + } + if (isNaN(Number(dateString))) { + return dateString; + } + const formattedDate = format( + new Date(Number(dateString)), + EXPECTED_DATE_FORMAT, + ); + return formattedDate; +}; + +export const dateToEpoch = (dateString: string): number => { + // Convert the Date object to epoch time (milliseconds since 1970/01/01 UTC) + const epoch = new Date( + isNaN(Number(dateString)) ? dateString : Number(dateString), + ).getTime(); + + return epoch; +}; + +export const convertToDateObject = ( + date: Date | string | null | undefined, +): Date | null => { + if (!date) return null; + + if (date instanceof Date) { + return isValid(date) ? date : null; + } + + const parsed = parseISO(date); + return isValid(parsed) ? parsed : null; +}; + +export const formatDate = ( + date: Date | string | number | null | undefined, + dateFormat: string, +): string => { + if (!date) return ""; + + let parsedDate: Date; + + if (typeof date === "number") { + parsedDate = date > 1e12 ? new Date(date) : fromUnixTime(date); + } else if (typeof date === "string") { + parsedDate = parseISO(date); + } else { + parsedDate = date; + } + + if (!isValid(parsedDate)) return ""; + + return format(parsedDate, dateFormat); +}; + +export const formatToDateTimeString = ( + date: Date | string | number | null | undefined, +): string => { + return formatDate(date, DATE_TIME_FORMAT); +}; + +export const getStartOfDayTime = (startDate: Date | null) => { + if (!startDate || !isValid(startDate)) return null; + return startOfDay(startDate).getTime(); +}; + +export const getEndOfDayTime = (endDate: Date | null) => { + if (!endDate || !isValid(endDate)) return null; + return endOfDay(endDate).getTime(); +}; + +export interface TimeRangeTimestamps { + start: number; + end: number; +} + +// Time unit mappings with their corresponding date-fns functions +const TIME_UNIT_MAP = { + // Minutes + min: subMinutes, + m: subMinutes, + minute: subMinutes, + minutes: subMinutes, + + // Hours + hr: subHours, + h: subHours, + hour: subHours, + hours: subHours, + + // Days + day: subDays, + days: subDays, + d: subDays, +} as const; + +type TimeUnit = keyof typeof TIME_UNIT_MAP; + +export const getTimeRangeTimestamps = (range: string): TimeRangeTimestamps => { + const now = new Date(); + const nowTimestamp = now.getTime(); + + if (!range) { + // Default to 24 hours + return { start: subDays(now, 1).getTime(), end: nowTimestamp }; + } + + // Clean up input: remove extra spaces and lowercase it + const cleanedRange = range.trim().toLowerCase(); + + // Split by whitespace to get parts + const parts = cleanedRange.split(/\s+/); + + // We expect exactly 2 parts: number and unit + if (parts.length !== 2) { + // Fallback to 24 hours + return { start: subDays(now, 1).getTime(), end: nowTimestamp }; + } + + const [valueStr, unitStr] = parts; + const value = Number(valueStr); + + // Check if value is a valid number + if (isNaN(value) || value <= 0) { + return { start: subDays(now, 1).getTime(), end: nowTimestamp }; + } + + // Check if unit is supported + if (!(unitStr in TIME_UNIT_MAP)) { + return { start: subDays(now, 1).getTime(), end: nowTimestamp }; + } + + const unit = unitStr as TimeUnit; + const subtractFunction = TIME_UNIT_MAP[unit]; + const startDate = subtractFunction(now, value); + + return { + start: startDate.getTime(), + end: nowTimestamp, + }; +}; + +/** + * Formats a Unix timestamp (in seconds) as 'HH:mm:ss'. + * @param unixSeconds Unix timestamp in seconds + * @returns Formatted time string (e.g., '13:45:30') + */ +export const formatUnixTimeToTimeString = (unixSeconds: number): string => { + return format(fromUnixTime(unixSeconds), "HH:mm:ss"); +}; + +/** + * Returns a Unix timestamp (in seconds) representing the time `hoursBack` ago from `fromTimestamp`. + * If `fromTimestamp` is not provided, it defaults to now. + */ +export const getUnixTimestampHoursAgo = ( + hoursBack: number, + fromTimestamp?: number, +): number => { + const fromDate = fromTimestamp ? new Date(fromTimestamp * 1000) : new Date(); + const date = subHours(fromDate, hoursBack); + return Math.floor(date.getTime() / 1000); +}; + +/** + * Returns the current Unix timestamp in seconds. + */ +export const getCurrentUnixTimestamp = (): number => { + return Math.floor(Date.now() / 1000); +}; + +/** + * Returns the number of seconds between two dates or timestamps. + * Accepts numbers (ms), strings (ISO), or Date objects. + */ +export const getDifferenceInSeconds = ( + from: Date | string | number, + to: Date | string | number, +): number => { + const fromDate = new Date(from); + const toDate = new Date(to); + + if (isNaN(fromDate.getTime()) || isNaN(toDate.getTime())) { + throw new Error("Invalid date input to getDifferenceInSeconds"); + } + + return differenceInSeconds(toDate, fromDate); +}; + +/** + * Returns a human-friendly, fuzzy description of the time difference between two dates or timestamps. + * + * @param from - The starting date/time. Can be a Date object, ISO string, or timestamp (ms). + * @param to - The ending date/time. Can be a Date object, ISO string, or timestamp (ms). Defaults to now. + * @returns A string describing the approximate duration between the two dates (e.g., "about a minute", "2 hours"). + * Returns an empty string if either input is invalid. + * + * @example + * ```ts + * humanizeDuration(Date.now() - 45000, Date.now()); // "about a minute" + * humanizeDuration('2023-01-01T00:00:00Z'); // relative to now, e.g., "over 2 years" + * ``` + */ +export const humanizeDuration = ( + from: Date | string | number, + to: Date | string | number = Date.now(), +): string => { + const fromDate = new Date(from); + const toDate = new Date(to); + + if (!isValid(fromDate) || !isValid(toDate)) return ""; + + return formatDistance(fromDate, toDate); +}; + +/** + * Returns the current date/time rounded down to the start of the current hour (minutes, seconds, ms = 0). + */ +export const startOfCurrentHour = (): Date => { + const now = new Date(); + return setMilliseconds(setSeconds(setMinutes(now, 0), 0), 0); +}; + +/** + * Adds specified number of minutes to a given date. + * @param date Date to add minutes to + * @param minutes number of minutes to add + * @returns new Date with minutes added + */ +export const addMinutesToDate = (date: Date, minutes: number): Date => { + return addMinutes(date, minutes); +}; + +/** + * Returns a timezone offset like "+05:30" or "-04:00" + * Equivalent to Moment's `.format("Z")` + */ +export const getMomentStyleOffset = ( + timeZone: string, + date: Date = new Date(), +): string => { + const zonedDate = utcToZonedTime(date, timeZone); + const offsetMinutes = -zonedDate.getTimezoneOffset(); + + const sign = offsetMinutes >= 0 ? "+" : "-"; + const abs = Math.abs(offsetMinutes); + const hours = Math.floor(abs / 60) + .toString() + .padStart(2, "0"); + const minutes = (abs % 60).toString().padStart(2, "0"); + + return `${sign}${hours}:${minutes}`; +}; + +/** + * Returns a short time zone abbreviation (like "PDT", "IST", etc.) + * Equivalent to Moment's `.format("zz")` + */ +export const getTimeZoneAbbreviation = ( + timeZone: string, + date: Date = new Date(), +): string => { + try { + const formatter = new Intl.DateTimeFormat("en-US", { + timeZone, + timeZoneName: "short", + }); + const parts = formatter.formatToParts(date); + const tzPart = parts.find((p) => p.type === "timeZoneName"); + return tzPart?.value ?? ""; + } catch { + return ""; + } +}; + +/** + * Returns a list of all time zones. + */ +export const getTimeZoneNames = (): string[] => { + return Intl.supportedValuesOf("timeZone"); +}; + +/** + * Guesses the system time zone + */ +export const guessUserTimeZone = (): string => { + return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC"; +}; + +/** + * Formats a date using timezone-aware formatting + * @param date Date to format + * @param formatString Format string (e.g., "MMM d, yyyy hh:mm a") + * @param timeZone Timezone to use (defaults to system timezone) + * @returns Formatted date string + */ +export const formatInTimeZone = ( + date: Date | string | number, + formatString: string, + timeZone?: string, +): string => { + // Use the native formatInTimeZone from date-fns-tz + if (timeZone) { + return formatInTz(date, timeZone, formatString); + } + // Fallback to regular format if no timezone specified + const dateObj = + typeof date === "string" || typeof date === "number" + ? new Date(date) + : date; + return formatTz(dateObj, formatString); +}; + +/** + * Converts a date to a Date object using timezone-aware parsing + * @param date Date to convert + * @param timeZone Timezone to use (defaults to system timezone) + * @returns Date object + */ +export const convertToDateInTimeZone = ( + date: Date | string | number, + timeZone?: string, +): Date => { + return toDate(date, { timeZone }); +}; + +/** + * Parses a date string that is in a specific timezone and converts it to a Date object (UTC internally) + * This is useful when you have a date string like "2024-10-17T15:30:00" that represents a time in a specific timezone + * @param dateString Date string to parse + * @param timeZone Timezone the date string is in + * @returns Date object (stored as UTC internally) + */ +export const parseDateInTimeZone = ( + dateString: string, + timeZone: string, +): Date => { + // If the string already has timezone info (Z or offset), just parse it normally + if (dateString.includes("Z") || /[+-]\d{2}:\d{2}$/.test(dateString)) { + return new Date(dateString); + } + // Otherwise, treat the string as being in the specified timezone + return zonedTimeToUtc(dateString, timeZone); +}; diff --git a/ui-next/src/utils/deprecatedRadioFilter.ts b/ui-next/src/utils/deprecatedRadioFilter.ts new file mode 100644 index 0000000000..0140ce57d1 --- /dev/null +++ b/ui-next/src/utils/deprecatedRadioFilter.ts @@ -0,0 +1,21 @@ +const options = [ + { + value: "graaljs", + label: "ECMASCRIPT", + }, + { + value: "javascript", + label: "Javascript(deprecated)", + disabled: true, + }, + { + value: "value-param", + label: "Value-Param", + }, +]; + +export const filterOptionByEvaluatorType = (evaluatorType?: string) => { + return options.filter( + (option) => option.value !== "javascript" || evaluatorType === "javascript", + ); +}; diff --git a/ui-next/src/utils/fieldHelpers.tsx b/ui-next/src/utils/fieldHelpers.tsx new file mode 100644 index 0000000000..67bee209e6 --- /dev/null +++ b/ui-next/src/utils/fieldHelpers.tsx @@ -0,0 +1,658 @@ +import { FormControlLabel, Grid, Link, Switch } from "@mui/material"; +import { useSelector } from "@xstate/react"; +import MuiTypography from "components/MuiTypography"; +import PromptVariables from "components/PromptVariables"; +import { ConductorAutocompleteVariables } from "components/v1/FlatMapForm/ConductorAutocompleteVariables"; +import { path as _path, clone, setWith } from "lodash/fp"; +import { ConductorValueInput } from "pages/definition/EditorPanel/TaskFormTab/forms/ConductorValueInput"; +import { ConductorArrayMapFormBase } from "pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields/ConductorArrayMapForm"; +import { + LLMFormFieldsEvents, + LLMFormFieldsMachineContext, + LLMFormFieldsMachineEventTypes, +} from "pages/definition/EditorPanel/TaskFormTab/forms/LLMFormFields/state"; +import { useGetSetHandler } from "pages/definition/EditorPanel/TaskFormTab/forms/useGetSetHandler"; +import { FunctionComponent, useMemo } from "react"; +import { TaskDef } from "types/common"; +import { UiIntegrationsFieldType } from "types/FormFieldTypes"; +import { ActorRef, State } from "xstate"; +import { MEDIA_TYPE_SUGGESTIONS } from "./constants/httpSuggestions"; + +export type FieldComponentType = FunctionComponent<{ + onChange: (t: Partial) => void; + actor: ActorRef; + task: Partial; +}>; + +const DEFAULT_VALUES_FOR_ARRAY = { object: [] }; + +// this was root of many issues. though fixed i would still get rid of JSONField +export const updateField = (path: string, value: any, taskJson: any) => { + return setWith(clone, path, value, clone(taskJson)); +}; + +const fieldValue = (task: Partial, type: string) => { + const value = _path(`inputParameters.${type}`, task); + if (value != null) { + return value; + } else if ( + type === UiIntegrationsFieldType.STOP_WORDS || + type === UiIntegrationsFieldType.EMBEDDINGS + ) { + return []; + } else return ""; +}; + +const useSetterGetter = ( + type: UiIntegrationsFieldType, + task: Partial, +) => [ + (value: unknown) => updateField(`inputParameters.${type}`, value, task), + fieldValue(task, type), +]; + +const aiFieldTypes = { + [UiIntegrationsFieldType.LLM_PROVIDER]: ({ onChange, task, actor }) => { + const options = useSelector(actor, (state) => + state.context.llmProviderOptions.map( + ({ name }: { name: string }) => name, // Fix types + ), + ); + const [setValue, ipValue] = useSetterGetter( + UiIntegrationsFieldType.LLM_PROVIDER, + task, + ); + return ( + <> + onChange(setValue(value))} + value={ipValue} + otherOptions={options} + label="LLM provider" + onFocus={() => + actor.send({ + type: LLMFormFieldsMachineEventTypes.FOCUS_LLM_PROVIDER, + task, + }) + } + /> + + ); + }, + [UiIntegrationsFieldType.MODEL]: ({ onChange, task, actor }) => { + const options = useSelector(actor, (state) => + state.context.modelOptions.map( + ({ api }: { api: string }) => api, // Fix types + ), + ); + const [setValue, ipValue] = useSetterGetter( + UiIntegrationsFieldType.MODEL, + task, + ); + return ( + onChange(setValue(value))} + value={ipValue} + otherOptions={options} + label="Model" + onFocus={() => + actor.send({ + type: LLMFormFieldsMachineEventTypes.FOCUS_MODEL, + task, + }) + } + /> + ); + }, + [UiIntegrationsFieldType.PROMPT_NAME]: ({ onChange, task, actor }) => { + const promptNames = useSelector( + actor, + (state) => state.context.promptNameOptions, + ); + const [options] = useMemo(() => { + return [ + promptNames.map( + ({ name }: { name: string }) => name, // Fix types + ), + ]; + }, [promptNames]); + + const [setValue, ipValue] = useSetterGetter( + UiIntegrationsFieldType.PROMPT_NAME, + task, + ); + + const currentVariables = task.inputParameters?.promptVariables || {}; + return ( + + + + Enter a saved AI Prompt name or{" "} + + create a new one. + + + { + actor.send({ + type: LLMFormFieldsMachineEventTypes.SELECT_PROMPT_NAME, + task: setValue(value), + }); + }} + value={ipValue} + otherOptions={options} + label="Prompt Name" + onFocus={() => + actor.send({ + type: LLMFormFieldsMachineEventTypes.FOCUS_PROMPT_NAMES, + task, + }) + } + /> + + + {`Variables to be used in the prompt (e.g. "What's the weather in {$userLocation}?").`} + + + + ); + }, + [UiIntegrationsFieldType.TEXT]: ({ onChange, task }) => { + const [setValue, ipValue] = useSetterGetter( + UiIntegrationsFieldType.TEXT, + task, + ); + return ( + onChange(setValue(value))} + value={ipValue} + label="Text" + /> + ); + }, + [UiIntegrationsFieldType.VECTOR_DB]: ({ onChange, task, actor }) => { + const options = useSelector(actor, (state) => + state.context.vectorDbOptions.map( + ({ name }: { name: string }) => name, // Fix types + ), + ); + const [setValue, ipValue] = useSetterGetter( + UiIntegrationsFieldType.VECTOR_DB, + task, + ); + return ( + onChange(setValue(value))} + value={ipValue} + otherOptions={options} + onFocus={() => + actor.send({ + type: LLMFormFieldsMachineEventTypes.FOCUS_VECTORDB, + task, + }) + } + label="Vector database" + inputProps={{ + tooltip: { + title: "Vector database", + content: "Enter the vector database for this task", + }, + }} + /> + ); + }, + [UiIntegrationsFieldType.NAMESPACE]: ({ onChange, task }) => { + const [setValue, ipValue] = useSetterGetter( + UiIntegrationsFieldType.NAMESPACE, + task, + ); + return ( + onChange(setValue(value))} + value={ipValue} + label="Namespace" + inputProps={{ + tooltip: { + title: "Namespace", + content: "Enter the namespace this task will utilize", + }, + }} + /> + ); + }, + [UiIntegrationsFieldType.QUERY]: ({ onChange, task }) => { + const [setValue, ipValue] = useSetterGetter( + UiIntegrationsFieldType.QUERY, + task, + ); + return ( + onChange(setValue(value))} + /> + ); + }, + [UiIntegrationsFieldType.ID]: ({ onChange, task }) => { + const [setValue, ipValue] = useSetterGetter( + UiIntegrationsFieldType.ID, + task, + ); + return ( + onChange(setValue(value))} + /> + ); + }, + [UiIntegrationsFieldType.EMBEDDING_MODEL_PROVIDER]: ({ + onChange, + task, + actor, + }) => { + const options = useSelector(actor, (state) => + state.context.llmProviderOptions.map( + ({ name }: { name: string }) => name, // Fix types + ), + ); + + const [setValue, ipValue] = useSetterGetter( + UiIntegrationsFieldType.EMBEDDING_MODEL_PROVIDER, + task, + ); + return ( + onChange(setValue(value))} + value={ipValue} + otherOptions={options} + label="Embedding model provider" + onFocus={() => + actor.send({ + type: LLMFormFieldsMachineEventTypes.FOCUS_LLM_PROVIDER, + task, + }) + } + /> + ); + }, + [UiIntegrationsFieldType.EMBEDDING_MODEL]: ({ onChange, task, actor }) => { + const options = useSelector(actor, (state) => + state.context.embeddingModelOptions.map( + ({ api }: { api: string }) => api, + ), + ); + + const [setValue, ipValue] = useSetterGetter( + UiIntegrationsFieldType.EMBEDDING_MODEL, + task, + ); + return ( + onChange(setValue(value))} + value={ipValue} + onFocus={() => + actor.send({ + type: LLMFormFieldsMachineEventTypes.FOCUS_EMBEDDINGS_MODEL, + task, + }) + } + otherOptions={options} + label="Embedding model" + /> + ); + }, + [UiIntegrationsFieldType.URL]: ({ onChange, task }) => { + const [setValue, ipValue] = useSetterGetter( + UiIntegrationsFieldType.URL, + task, + ); + return ( + onChange(setValue(value))} + value={ipValue} + label="URL" + /> + ); + }, + [UiIntegrationsFieldType.MEDIA_TYPE]: ({ onChange, task }) => { + const [setValue, ipValue] = useSetterGetter( + UiIntegrationsFieldType.MEDIA_TYPE, + task, + ); + return ( + onChange(setValue(value))} + value={ipValue} + otherOptions={MEDIA_TYPE_SUGGESTIONS} + label="Media type" + /> + ); + }, + [UiIntegrationsFieldType.DOC_ID]: ({ onChange, task }) => { + const [setValue, ipValue] = useSetterGetter( + UiIntegrationsFieldType.DOC_ID, + task, + ); + return ( + onChange(setValue(value))} + value={ipValue} + label="Doc ID" + /> + ); + }, + [UiIntegrationsFieldType.TEMPERATURE]: ({ onChange, task }) => { + const [setValue, ipValue] = useSetterGetter( + UiIntegrationsFieldType.TEMPERATURE, + task, + ); + return ( + onChange(setValue(value))} + value={ipValue} + label="Temperature" + coerceTo="double" + /> + ); + }, + [UiIntegrationsFieldType.TOP_P]: ({ onChange, task }) => { + const [setValue, ipValue] = useSetterGetter( + UiIntegrationsFieldType.TOP_P, + task, + ); + return ( + onChange(setValue(value))} + value={ipValue} + label="TopP" + coerceTo="double" + /> + ); + }, + [UiIntegrationsFieldType.MAX_RESULTS]: ({ onChange, task }) => { + const [setValue, ipValue] = useSetterGetter( + UiIntegrationsFieldType.MAX_RESULTS, + task, + ); + return ( + onChange(setValue(value))} + value={ipValue} + label="Max Results" + coerceTo="integer" + /> + ); + }, + [UiIntegrationsFieldType.MAX_TOKENS]: ({ onChange, task }) => { + const [setValue, ipValue] = useSetterGetter( + UiIntegrationsFieldType.MAX_TOKENS, + task, + ); + return ( + onChange(setValue(value))} + value={ipValue} + coerceTo="integer" + label="Token limit" + inputProps={{ + tooltip: { + title: "Token limit", + content: + "Maximum no. of tokens to return as part of result (a token is approximately 4 characters)", + }, + }} + /> + ); + }, + [UiIntegrationsFieldType.STOP_WORDS]: ({ onChange, task }) => { + const [stopWordsVal, handleStopWordsVal] = useGetSetHandler( + { onChange, task }, + `inputParameters.stopWords`, + ); + + return ( + { + handleStopWordsVal(val); + }} + defaultObjectValue={DEFAULT_VALUES_FOR_ARRAY} + /> + ); + }, + [UiIntegrationsFieldType.INDEX]: ({ onChange, task, actor }) => { + const options = useSelector( + actor, + (state: State) => + state.context.indexOptions.map( + ({ api }: { api: string }) => api, // Fix types + ), + ); + + const [setValue, ipValue] = useSetterGetter( + UiIntegrationsFieldType.INDEX, + task, + ); + return ( + onChange(setValue(value))} + value={ipValue} + onFocus={() => + actor.send({ + type: LLMFormFieldsMachineEventTypes.FOCUS_INDEX, + task, + }) + } + label="Index" + otherOptions={options} + /> + ); + }, + [UiIntegrationsFieldType.EMBEDDINGS]: ({ onChange, task }) => { + const [setValue, ipValue] = useSetterGetter( + UiIntegrationsFieldType.EMBEDDINGS, + task, + ); + + return ( + onChange(setValue(value))} + value={ipValue} + label="Embeddings" + /> + ); + }, + [UiIntegrationsFieldType.CHUNK_SIZE]: ({ onChange, task }) => { + const [setValue, ipValue] = useSetterGetter( + UiIntegrationsFieldType.CHUNK_SIZE, + task, + ); + return ( + onChange(setValue(value))} + value={ipValue} + label="Chunk Size" + coerceTo="integer" + /> + ); + }, + [UiIntegrationsFieldType.CHUNK_OVERLAP]: ({ onChange, task }) => { + const [setValue, ipValue] = useSetterGetter( + UiIntegrationsFieldType.CHUNK_OVERLAP, + task, + ); + return ( + onChange(setValue(value))} + value={ipValue} + label="Chunk overlap" + coerceTo="integer" + /> + ); + }, + + [UiIntegrationsFieldType.INSTRUCTIONS]: ({ onChange, task, actor }) => { + const options = useSelector( + actor, + (state: State) => + state.context.promptNameOptions.map(({ name }) => name), + ); + + const [setValue, ipValue] = useSetterGetter( + UiIntegrationsFieldType.INSTRUCTIONS, + task, + ); + + const currentVariables = task.inputParameters?.promptVariables || {}; + return ( + + + + Enter a saved AI Prompt name or{" "} + + create a new one. + + + { + actor.send({ + type: LLMFormFieldsMachineEventTypes.SELECT_INSTRUCTIONS, + task: setValue(value), + }); + }} + onFocus={() => + actor.send({ + type: LLMFormFieldsMachineEventTypes.FOCUS_PROMPT_NAMES, + task, + }) + } + openOnFocus + /> + + + {`Variables to be used in the prompt (e.g. "What's the weather in {$userLocation}?").`} + + + + ); + }, + [UiIntegrationsFieldType.MESSAGES]: ({ onChange, task }) => { + const [setValue, ipValue] = useSetterGetter( + UiIntegrationsFieldType.MESSAGES, + task, + ); + + return ( + <> + + Messages with roles such as user, assistant, system, etc. + + onChange(setValue(value))} + /> + + ); + }, + [UiIntegrationsFieldType.JSON_OUTPUT]: ({ onChange, task }) => { + const [setValue, ipValue] = useSetterGetter( + UiIntegrationsFieldType.JSON_OUTPUT, + task, + ); + + return ( + <> + onChange(setValue(event.target.checked))} + /> + } + label="Enable JSON Output" + sx={{ + "& .MuiFormControlLabel-label": { + fontWeight: 600, + color: "#767676", + }, + }} + /> + + When enabled, the LLM response will be parsed as JSON. This is useful + when you expect the model to return structured data. + + + ); + }, + [UiIntegrationsFieldType.DIMENSIONS]: ({ onChange, task }) => { + const [setValue, ipValue] = useSetterGetter( + UiIntegrationsFieldType.DIMENSIONS, + task, + ); + return ( + onChange(setValue(value))} + value={ipValue} + label="Dimensions" + coerceTo="integer" + /> + ); + }, +} satisfies Record; + +const aIFormField = (type: UiIntegrationsFieldType): FieldComponentType => + aiFieldTypes[type]; + +export const fieldsToFieldsFieldsComponents = ( + fields: UiIntegrationsFieldType[], +): Array<[UiIntegrationsFieldType, FieldComponentType]> => + fields.map((field) => [field, aIFormField(field)]); diff --git a/ui-next/src/utils/flags.ts b/ui-next/src/utils/flags.ts new file mode 100644 index 0000000000..9e5a4f9f2a --- /dev/null +++ b/ui-next/src/utils/flags.ts @@ -0,0 +1,134 @@ +import _merge from "lodash/merge"; +declare global { + interface Window { + conductor: any; + heap?: any; + authConfig?: { + domain?: string; + clientId?: string; + useIdToken?: boolean; + audience?: string; + type?: string; + issuer?: string; + idps?: any[]; //Typing as any. else would need to import okta types. and this is Oidc + useInteractionCodeFlow?: boolean; + authorizationEndpoint?: string; + tokenEndpoint?: string; + endSessionEndpoint?: string; + }; + auth0Identifiers?: { + domain?: string; + clientId?: string; + }; + } +} + +export const FEATURES = Object.freeze({ + ACCESS_MANAGEMENT: "ACCESS_MANAGEMENT", + COPY_TOKEN: "COPY_TOKEN", + TASK_VISIBILITY: "TASK_VISIBILITY", + PLAYGROUND: "PLAYGROUND", + SCHEDULER: "SCHEDULER", + CREATOR_ENABLE_CREATOR: "CREATOR_ENABLE_CREATOR", + CREATOR_ENABLE_REAFLOW_DIAGRAM: "CREATOR_ENABLE_REAFLOW_DIAGRAM", + ENABLE_DARK_MODE_TOGGLE: "ENABLE_DARK_MODE_TOGGLE", + NAVBAR_ELEMENTS_VARIANT: "NAVBAR_ELEMENTS_VARIANT", + SHOW_START_TITLE: "SHOW_START_TITLE", + SHOW_CLOUD_LINK: "SHOW_CLOUD_LINK", + SHOW_FEEDBACK_FORM: "SHOW_FEEDBACK_FORM", + SHOW_SUPPORT_FORM: "SHOW_SUPPORT_FORM", + SHOW_DOCUMENTATION: "SHOW_DOCUMENTATION", + SHOW_JOIN_SLACK_COMMUNITY: "SHOW_JOIN_SLACK_COMMUNITY", + BETA_KEYBOARD_FLOW: "BETA_KEYBOARD_FLOW", + ENABLE_METRICS_DASHBOARD: "ENABLE_METRICS_DASHBOARD", + METRICS_ORIGIN_URL: "METRICS_ORIGIN_URL", + HIDE_JAVASCRIPT_OPTION: "HIDE_JAVASCRIPT_OPTION", + HUMAN_TASK: "HUMAN_TASK", + LOGIN_REDIRECT_TYPE: "LOGIN_REDIRECT_TYPE", + DRAG_DROP_TASK_INCREMENT_THRESHOLD: "DRAG_DROP_TASK_INCREMENT_THRESHOLD", + INTEGRATIONS: "INTEGRATIONS", + INTEGRATIONS_FLAT_LAYOUT: "INTEGRATIONS_FLAT_LAYOUT", + ANNOUNCEMENT_EXPIRY_DATE: "ANNOUNCEMENT_EXPIRY_DATE", + SHOW_NEWS_ICON: "SHOW_NEWS_ICON", + HEAP_APP_ID: "HEAP_APP_ID", + DISABLE_EXPAND_WORKFLOW: "ENABLE_EXPAND_WORKFLOW", + DISABLE_TASK_STATS: "ENABLE_TASK_STATS", + ENABLE_TASK_DEFINITION_FORM: "ENABLE_TASK_DEFINITION_FORM", + SECRETS: "SECRETS", + WEBHOOKS: "WEBHOOKS", + RBAC: "RBAC", + SHOW_ONBOARDING_QUIZ: "SHOW_ONBOARDING_QUIZ", + SKU_ENABLED: "SKU_ENABLED", + TASK_INDEXING: "TASK_INDEXING", + TRIGGER_WORKFLOW: "TRIGGER_WORKFLOW", + ENV_IS_PRODUCTION: "ENV_IS_PRODUCTION", + ENABLE_WHITE_BACKGROUND_FORM: "ENABLE_WHITE_BACKGROUND_FORM", + ADVANCED_ERROR_INSPECTOR_VALIDATIONS: "ADVANCED_ERROR_INSPECTOR_VALIDATIONS", + CUSTOM_LOGO_URL: "CUSTOM_LOGO_URL", + SHOW_END_TIME_IN_DATEPICKER: "SHOW_END_TIME_IN_DATEPICKER", + SHOW_EVENT_MONITOR: "SHOW_EVENT_MONITOR", + SENDGRID_TASK: "SENDGRID_TASK", + LOG_ROCKET_KEY: "LOG_ROCKET_KEY", + SHOW_AI_STUDIO_BANNER_FLAG: "SHOW_AI_STUDIO_BANNER_FLAG", + SHOW_GET_STARTED_PAGE: "SHOW_GET_STARTED_PAGE", + GET_STARTED_VIDEO_URL: "GET_STARTED_VIDEO_URL", + REMOTE_SERVICES: "REMOTE_SERVICES", + MULTITENANCY_TYPE: "MULTITENANCY_TYPE", + DEFAULT_ROLES: "DEFAULT_ROLES", + HIDE_IMPORT_BPMN: "HIDE_IMPORT_BPMN", + GATEWAY_ENABLED: "GATEWAY_ENABLED", + CLOUD_TEMPLATES_SOURCE: "CLOUD_TEMPLATES_SOURCE", + AI_PROMPTS_VERSIONING: "AI_PROMPTS_VERSIONING", + GROWTHBOOK_CLIENT_KEY: "GROWTHBOOK_CLIENT_KEY", + ENABLE_RERUN_FROM_FORK_AND_DOWHILE_TASKS: + "ENABLE_RERUN_FROM_FORK_AND_DOWHILE_TASKS", + GOOGLE_CLIENT_ID: "GOOGLE_CLIENT_ID", + NOTIFY_HUMAN_TASK: "NOTIFY_HUMAN_TASK", + WORKFLOW_INTROSPECTION: "WORKFLOW_INTROSPECTION", + ENABLE_CONFETTI: "ENABLE_CONFETTI", + OIDC_REMOVE_OFFLINE_ACCESS: "OIDC_REMOVE_OFFLINE_ACCESS", + SHOW_ROLES_MENU_ITEM: "SHOW_ROLES_MENU_ITEM", + SHOW_AGENT: "SHOW_AGENT", + ENABLE_AGENT_AUDIO_INPUT: "ENABLE_AGENT_AUDIO_INPUT", + AI_CODER_WORKER: "AI_CODER_WORKER", + AI_CODER_CLOUD_WORKER: "AI_CODER_CLOUD_WORKER", + TAG_VISIBILITY: "TAG_VISIBILITY", +}); + +const mapOfLocalStorageValues = Object.fromEntries( + Object.entries(FEATURES).map(([k, v]) => [ + k, + localStorage.getItem(v) === null ? undefined : localStorage.getItem(v), + ]), +); +const mapOfEnvValues = Object.fromEntries( + Object.entries(FEATURES).map(([k, v]) => [ + k, + process.env[`REACT_APP_FEATURE_${v}`], + ]), +); +const mapOfContextJs = Object.fromEntries( + Object.entries(FEATURES).map(([k, v]) => [ + k, + window.conductor && window.conductor[v], + ]), +); + +const result = _merge( + {}, + mapOfContextJs, + mapOfEnvValues, + mapOfLocalStorageValues, +); + +export const featureFlags = { + isEnabled: (feature: string) => { + return result[feature] === "true" || result[feature] === true; + }, + getValue: (feature: string, defaultValue?: string) => { + return result[feature] || defaultValue; + }, + getContextValue: (feature: string) => { + return window.conductor && window.conductor[feature]; + }, +}; diff --git a/ui-next/src/utils/gtag.ts b/ui-next/src/utils/gtag.ts new file mode 100644 index 0000000000..04c413b16a --- /dev/null +++ b/ui-next/src/utils/gtag.ts @@ -0,0 +1,88 @@ +// This util means to replace gtag +import { useEffect } from "react"; +import { featureFlags, FEATURES } from "utils/flags"; + +declare global { + interface Window { + gtag: any; + dataLayer: any; + } +} + +interface EventParams { + user_uuid?: string; + workflow_name?: string; + user_performed_action?: string; + error_type?: string; + event?: object; + start_time?: number; + end_time?: number; + item_id?: string; +} + +type SimpleUserInfo = { + uuid?: string; + user?: any; + id?: string; +}; + +export const GTAG_LABEL = "G-6DLM7JND12"; + +const isPlayground = featureFlags.isEnabled(FEATURES.PLAYGROUND); +const gtagAbstract = (event_name: string, event_params: EventParams) => { + if (isPlayground && window && window.gtag) { + window.gtag("event", event_name, { + ...event_params, + ...(process.env.NODE_ENV === "development" ? { debug_mode: true } : {}), + }); + } +}; + +export const useConfigureGtagUserIdIfPlayground = ( + conductorUser?: SimpleUserInfo, +) => { + useEffect(() => { + if (isPlayground && window && window.gtag && conductorUser?.id) { + window.gtag("config", GTAG_LABEL, { + user_id: conductorUser.id, + }); + } + }, [conductorUser?.id]); +}; +// flatten a given nested object +type FlattenedObject = Record; + +const flattenGtagObject = ( + obj: Record, + prefix = "", +): FlattenedObject => { + let result: FlattenedObject = {}; + + for (const key in obj) { + const newKey = prefix ? `${prefix}_${key}` : key; + + if ( + typeof obj[key] === "object" && + obj[key] !== null && + !Array.isArray(obj[key]) + ) { + const flattenedNestedObject = flattenGtagObject(obj[key], newKey); + result = { ...result, ...flattenedNestedObject }; + } else if ( + key === "crumbs" && + Array.isArray(obj[key]) && + obj[key].length > 0 + ) { + for (let i = 0; i < obj[key].length; i++) { + const newItem = obj[key][i].ref; + result[`${newKey}_${obj[key][i].refIdx}`] = newItem; + } + } else { + result[newKey] = obj[key]; + } + } + + return result; +}; + +export { gtagAbstract, flattenGtagObject }; diff --git a/ui-next/src/utils/handleValidChars.ts b/ui-next/src/utils/handleValidChars.ts new file mode 100644 index 0000000000..7e6ec0869d --- /dev/null +++ b/ui-next/src/utils/handleValidChars.ts @@ -0,0 +1,32 @@ +import { ChangeEvent } from "react"; +import { TITLE_ALLOWED_CHARS } from "./constants/common"; +/** + * If chars are valid, call handler + * @param handler + * @param regExVal + * @returns + */ +export const handleValidChars = + (handler: (val: string) => void, regExVal: string = TITLE_ALLOWED_CHARS) => + (value: string) => { + const regEx = new RegExp(regExVal); + if (regEx.test(value)) { + handler(value); + } + }; + +export const handleValidCharsForEvents = + ( + handler: (evt: ChangeEvent) => void, + regExVal: string = TITLE_ALLOWED_CHARS, + ) => + (ov: ChangeEvent) => { + const value = ov.target.value; + const regEx = new RegExp(regExVal); + if (regEx.test(value)) { + handler({ + ...ov, + target: { value }, + } as ChangeEvent); + } + }; diff --git a/ui-next/src/utils/helpers.ts b/ui-next/src/utils/helpers.ts new file mode 100644 index 0000000000..05b47ea745 --- /dev/null +++ b/ui-next/src/utils/helpers.ts @@ -0,0 +1,261 @@ +import _isInteger from "lodash/isInteger"; +import { HumanTaskState as TaskState } from "types/HumanTaskTypes"; +import { colors } from "theme/tokens/variables"; +import { WorkflowExecutionStatus } from "types/Execution"; +import { TaskStatus } from "types/TaskStatus"; +import { + FIELD_TYPE_BOOLEAN, + FIELD_TYPE_NULL, + FIELD_TYPE_NUMBER, + FIELD_TYPE_OBJECT, + FIELD_TYPE_STRING, + FieldType, +} from "types/common"; +import { WORKFLOW_SCHEDULE_EXECUTION_STATE } from "utils/constants/workflowScheduleExecution"; + +export function isFailedTask(status: TaskStatus) { + return ( + status === "FAILED" || + status === "FAILED_WITH_TERMINAL_ERROR" || + status === "TIMED_OUT" || + status === "CANCELED" + ); +} + +/** + * Create data table title via search result + * @param {array} filteredData: data after filtering or searching + * @param {array} data: data of table + * @returns {string} + */ +export function createTableTitle({ + filteredData = [], + data = [], +}: { + filteredData: any[]; + data: any[]; +}): string { + return filteredData.length === data.length + ? `${filteredData.length} results` + : `${filteredData.length} results (${ + data.length - filteredData.length + } not shown)`; +} + +export function juxt( + ...fns: readonly ((...args: T) => unknown)[] +): (...args: T) => unknown[] { + return (...args: T): unknown[] => { + return fns.map((fn) => fn(...args)); + }; +} + +/** + * Download file + * @param {object} data + * @param {string} fileName + * @param {string} type + */ +export type ExportableObject = { + data: Record; + fileName: string; + type: string; +}; + +export const exportObjToFile = ({ + data, + fileName, + type = "application/json", +}: ExportableObject) => { + const a = window.document.createElement("a"); + + a.href = window.URL.createObjectURL( + new Blob([JSON.stringify(data, null, 2)], { + type, + }), + ); + a.download = fileName; + + // Append anchor to body. + document.body.appendChild(a); + a.click(); + + // Remove anchor from body + document.body.removeChild(a); +}; + +const statusColor = { + success: colors.successTag, + progress: colors.progressTag, + error: colors.errorTag, + warning: colors.warningTag, +} as const; + +/** + * Get color for rendering chip status + * @param {string} status: item's status (ex: workflow's status...) + * @returns {string} + */ +export const getChipStatusColor = ( + status: TaskStatus | WorkflowExecutionStatus | TaskState, +) => { + switch (status) { + case WorkflowExecutionStatus.RUNNING: + case TaskStatus.IN_PROGRESS: + case TaskStatus.SCHEDULED: + case TaskState.ASSIGNED: + case WORKFLOW_SCHEDULE_EXECUTION_STATE.POLLED: + return statusColor.progress; + + case WorkflowExecutionStatus.COMPLETED: + case TaskStatus.COMPLETED: + case WORKFLOW_SCHEDULE_EXECUTION_STATE.EXECUTED: + return statusColor.success; + + case WorkflowExecutionStatus.PAUSED: + case TaskStatus.SKIPPED: + case TaskStatus.CANCELED: + case TaskStatus.PENDING: + return statusColor.warning; + + default: + return statusColor.error; + } +}; + +/** + * Open link in new tab + * @param {string} url + */ +export const openInNewTab = (url: string) => { + const newWindow = window.open(url, "_blank", "noopener,noreferrer"); + + if (newWindow) newWindow.opener = null; +}; + +export const inferType: (value: any) => FieldType = (value: any) => { + if (value === null) { + return FIELD_TYPE_NULL; + } + + if (typeof value === "number") { + return FIELD_TYPE_NUMBER; + } + + if (typeof value === "object") { + return FIELD_TYPE_OBJECT; + } + + if (typeof value === "boolean") { + return FIELD_TYPE_BOOLEAN; + } + + return FIELD_TYPE_STRING; +}; + +export type ValueInputDefaultValues = Partial>; + +export const DEFAULT_FIELD_VALUES_CONF: Record = { + [FIELD_TYPE_STRING]: "", + [FIELD_TYPE_NUMBER]: 0, + [FIELD_TYPE_OBJECT]: {}, + [FIELD_TYPE_BOOLEAN]: false, + [FIELD_TYPE_NULL]: null, +}; + +export const castToType = ( + value: any, + type: FieldType, + defaultValuesProvided: ValueInputDefaultValues = DEFAULT_FIELD_VALUES_CONF, +) => { + const defaultValues = { + ...DEFAULT_FIELD_VALUES_CONF, + ...defaultValuesProvided, + }; + if (type === FIELD_TYPE_NUMBER) { + try { + if (!isNaN(value)) { + return Number(value); + } + + return defaultValues[FIELD_TYPE_NUMBER]; + } catch { + return defaultValues[FIELD_TYPE_NUMBER]; + } + } + + if (type === FIELD_TYPE_OBJECT) { + try { + return typeof value !== "boolean" && value !== null && isNaN(value) + ? JSON.parse(value) + : defaultValues[FIELD_TYPE_OBJECT]; + } catch { + return defaultValues[FIELD_TYPE_OBJECT]; + } + } + + if (type === FIELD_TYPE_BOOLEAN) { + return Boolean(value); + } + + if (type === FIELD_TYPE_NULL) { + return null; + } + return typeof value === "string" ? value : defaultValues[FIELD_TYPE_STRING]; +}; + +export const checkCoerceTypeError = ({ + value, + coerceTo, +}: { + value: any; + coerceTo: any; +}) => { + // Don't check reference string + if (typeof value === "string" && value.startsWith("${")) { + return false; + } + + const tempValue = castToType( + value, + isNaN(value as any) ? FIELD_TYPE_STRING : FIELD_TYPE_NUMBER, + ); + const valueType = inferType(tempValue); + + const isIntegerValue = _isInteger(tempValue); + + if (coerceTo === "integer") { + return !isIntegerValue; + } + + if (coerceTo === "double") { + return valueType !== FIELD_TYPE_NUMBER; + } + + return false; +}; + +export function replacePathPlaceholdersToWorkflowInput(path: string): string { + return path.replace(/\{(\w+)\}/g, (_, key) => `\${workflow.input.${key}}`); +} + +export const parseErrorResponse = async ({ + response, + module, + operation = "performing this operation", +}: { + response: Response; + module: string; + operation?: string; +}): Promise => { + try { + const json = await response?.json(); + if (json?.message) { + return json.message; + } else { + return `An error occurred while ${operation} on ${module}`; + } + } catch { + return `An error occurred while ${operation} on ${module}`; + } +}; diff --git a/ui-next/src/utils/hooks/index.ts b/ui-next/src/utils/hooks/index.ts new file mode 100644 index 0000000000..feaca210bd --- /dev/null +++ b/ui-next/src/utils/hooks/index.ts @@ -0,0 +1,5 @@ +export * from "./useCustomPagination"; +export * from "./useEventNameSuggestions"; +export * from "./useBatchedTagsData"; +export * from "./useGetIntegrations"; +export * from "./useWorkflowNamesAndVersionsQuery"; diff --git a/ui-next/src/utils/hooks/useAutoCompleteInputValidation.ts b/ui-next/src/utils/hooks/useAutoCompleteInputValidation.ts new file mode 100644 index 0000000000..6339e53c0e --- /dev/null +++ b/ui-next/src/utils/hooks/useAutoCompleteInputValidation.ts @@ -0,0 +1,15 @@ +import { useState } from "react"; + +export const useAutoCompleteInputValidation = (initialValue = "") => { + const [value, setValue] = useState(initialValue); + const [isFocused, setFocused] = useState(false); + const hasError = !!value && !isFocused; + + return { + value, + setValue, + isFocused, + setFocused, + hasError, + }; +}; diff --git a/ui-next/src/utils/hooks/useBatchedTagsData.ts b/ui-next/src/utils/hooks/useBatchedTagsData.ts new file mode 100644 index 0000000000..98f69f9aa3 --- /dev/null +++ b/ui-next/src/utils/hooks/useBatchedTagsData.ts @@ -0,0 +1,34 @@ +import { useQuery } from "react-query"; +import { fetchWithContext, useFetchContext } from "plugins/fetch"; +import { useAuthHeaders } from "utils/query"; + +// Custom hook to fetch grouped tags data +export const useBatchedTagsData = () => { + const fetchContext = useFetchContext(); + const fetchParams = { headers: useAuthHeaders() }; + + return useQuery( + [fetchContext.stack, "tags-dashboard-batch"], + async () => { + const groupedTags = await fetchWithContext( + "/metadata/tags/grouped", + fetchContext, + fetchParams, + ); + + return groupedTags || []; + }, + { + enabled: fetchContext.ready, + keepPreviousData: true, + staleTime: 30000, // 30 seconds + retry: (failureCount, error: unknown) => { + const status = (error as { status?: number })?.status; + if (status && status >= 400 && status < 500) { + return false; + } + return failureCount < 3; + }, + }, + ); +}; diff --git a/ui-next/src/utils/hooks/useConductorProjectBuilder.ts b/ui-next/src/utils/hooks/useConductorProjectBuilder.ts new file mode 100644 index 0000000000..c71291fc9e --- /dev/null +++ b/ui-next/src/utils/hooks/useConductorProjectBuilder.ts @@ -0,0 +1,256 @@ +import { + CodeLanguage, + JavaLanguageSet, +} from "components/GetStartedSample/types"; +import { useState, useEffect, useCallback } from "react"; + +interface UseConductorProjectBuilderOptionsBase { + apiKey?: string; + apiSecret?: string; + serverUrl: string; + language: CodeLanguage; + taskName: string; + useEnvVars: boolean; +} + +interface UseConductorProjectBuilderOptionsJava extends UseConductorProjectBuilderOptionsBase { + language: CodeLanguage.JAVA; + languageSet: JavaLanguageSet; + projectName?: string; + packageName?: string; +} + +interface UseConductorProjectBuilderOptionsGo extends UseConductorProjectBuilderOptionsBase { + language: CodeLanguage.GO; +} + +interface UseConductorProjectBuilderOptionsPython extends UseConductorProjectBuilderOptionsBase { + language: CodeLanguage.PYTHON; +} + +interface UseConductorProjectBuilderOptionsJavaScript extends UseConductorProjectBuilderOptionsBase { + language: CodeLanguage.JS; +} + +interface UseConductorProjectBuilderOptionsCSharp extends UseConductorProjectBuilderOptionsBase { + language: CodeLanguage.CSHARP; + namespace?: string; +} + +interface UseConductorProjectBuilderOptionsClojure extends UseConductorProjectBuilderOptionsBase { + language: CodeLanguage.CLOJURE; +} + +interface UseConductorProjectBuilderOptionsGroovy extends UseConductorProjectBuilderOptionsBase { + language: CodeLanguage.GROOVY; + packageName?: string; +} + +type UseConductorProjectBuilderOptions = + | UseConductorProjectBuilderOptionsJava + | UseConductorProjectBuilderOptionsGo + | UseConductorProjectBuilderOptionsPython + | UseConductorProjectBuilderOptionsJavaScript + | UseConductorProjectBuilderOptionsCSharp + | UseConductorProjectBuilderOptionsClojure + | UseConductorProjectBuilderOptionsGroovy; + +interface UseConductorProjectBuilderReturn { + displayCode: string; + onDownload: () => Promise; +} + +const BASE_URL = "https://m9mk8uem2r.us-east-1.awsapprunner.com/"; + +export const useConductorProjectBuilder = ( + options: UseConductorProjectBuilderOptions, +): UseConductorProjectBuilderReturn => { + const { apiKey, apiSecret, serverUrl, language, taskName, useEnvVars } = + options; + const [displayCode, setDisplayCode] = useState(""); + + const getUrl = useCallback( + (isCode: boolean) => { + const url = new URL(BASE_URL); + + if (language === CodeLanguage.JAVA) { + const { languageSet, projectName, packageName } = options; + + if (languageSet === JavaLanguageSet.GRADLE) { + url.pathname = isCode + ? "project/worker/java/file/HelloWorldWorker.java" + : "project/worker/java/project.zip"; + } else if (languageSet === JavaLanguageSet.SPRING_GRADLE) { + url.pathname = isCode + ? "project/worker/spring/file/Worker.java" + : "project/worker/spring/project.zip"; + } + + if (isCode) { + if (projectName) { + url.searchParams.append("projectName", projectName); + } + if (packageName) { + url.searchParams.append("packageName", packageName); + } + } + } + + if (language === CodeLanguage.GO) { + url.pathname = isCode + ? "project/worker/go/file/worker.go" + : "project/worker/go/project.zip"; + } + + if (language === CodeLanguage.PYTHON) { + url.pathname = isCode + ? "project/worker/python/file/worker.py" + : "project/worker/python/project.zip"; + } + + if (language === CodeLanguage.JS) { + url.pathname = isCode + ? "project/worker/javascript/file/worker.js" + : "project/worker/javascript/project.zip"; + } + + if (language === CodeLanguage.CSHARP) { + const { namespace } = options; + + url.pathname = isCode + ? "project/worker/csharp/file/Worker.cs" + : "project/worker/csharp/project.zip"; + + if (isCode) { + if (namespace) { + url.searchParams.append("namespace", namespace); + } + } + } + + if (language === CodeLanguage.CLOJURE) { + url.pathname = isCode + ? "project/worker/clojure/file/core.clj" + : "project/worker/clojure/project.zip"; + } + + if (language === CodeLanguage.GROOVY) { + const { packageName } = options; + + url.pathname = isCode + ? "project/worker/groovydsl/file/worker.groovy" + : "project/worker/groovydsl/project.zip"; + + if (isCode) { + if (packageName) { + url.searchParams.append("packageName", packageName); + } + } + } + + if (isCode) { + if (useEnvVars) { + url.searchParams.append("useEnvVars", useEnvVars.toString()); + } else { + if (apiKey) { + url.searchParams.append("keyId", apiKey); + } + if (apiSecret) { + url.searchParams.append("secret", apiSecret); + } + } + url.searchParams.append("serverUrl", serverUrl); + url.searchParams.append("taskName", taskName); + } + + return url; + }, + [language, useEnvVars, apiKey, apiSecret, serverUrl, taskName, options], + ); + + // Function to fetch the project code based on user inputs + const fetchProjectCode = useCallback(async () => { + try { + const response = await fetch(getUrl(true), { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`Error fetching project code: ${response.statusText}`); + } + + const code = await response.text(); + setDisplayCode(code || "No code available"); + } catch (error) { + console.error("Error fetching project code:", error); + setDisplayCode("Error fetching project code."); + } + }, [getUrl]); + + // Function to download the project as a file + const onDownload = useCallback(async () => { + try { + const requestBody = { + ...(apiKey && { keyId: apiKey }), + ...(apiSecret && { secret: apiSecret }), + ...(serverUrl && { serverUrl }), + ...(taskName && { taskName }), + ...(useEnvVars && { useEnvVars }), + ...(options.language === CodeLanguage.JAVA && { + projectName: + (options as UseConductorProjectBuilderOptionsJava).projectName || + undefined, + packageName: + (options as UseConductorProjectBuilderOptionsJava).packageName || + undefined, + }), + ...(options.language === CodeLanguage.CSHARP && { + namespace: + (options as UseConductorProjectBuilderOptionsCSharp).namespace || + undefined, + }), + ...(options.language === CodeLanguage.GROOVY && { + packageNacme: + (options as UseConductorProjectBuilderOptionsGroovy).packageName || + undefined, + }), + }; + + const response = await fetch(getUrl(false), { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(requestBody), + }); + + if (!response.ok) { + throw new Error(`Error downloading project: ${response.statusText}`); + } + + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.setAttribute("download", `project.zip`); + document.body.appendChild(link); + link.click(); + link.remove(); + } catch (error) { + console.error("Error downloading project:", error); + } + }, [apiKey, apiSecret, serverUrl, taskName, useEnvVars, options, getUrl]); + + useEffect(() => { + const delay = setTimeout(() => { + fetchProjectCode(); + }, 500); // 500ms debounce delay + + return () => clearTimeout(delay); + }, [fetchProjectCode]); + + return { displayCode, onDownload }; +}; diff --git a/ui-next/src/utils/hooks/useCustomPagination.ts b/ui-next/src/utils/hooks/useCustomPagination.ts new file mode 100644 index 0000000000..40ededd544 --- /dev/null +++ b/ui-next/src/utils/hooks/useCustomPagination.ts @@ -0,0 +1,44 @@ +import { useCallback } from "react"; +import { useQueryState } from "react-router-use-location-state"; +import { + FILTER_QUERY_PARAM, + PAGE_QUERY_PARAM, + SEARCH_QUERY_PARAM, +} from "utils/constants/common"; + +const useCustomPagination = () => { + const [filterParam, setFilterParam] = useQueryState(FILTER_QUERY_PARAM, ""); + const [pageParam, setPageParam] = useQueryState(PAGE_QUERY_PARAM, ""); + const [searchParam, setSearchParam] = useQueryState(SEARCH_QUERY_PARAM, ""); + + const handleSearchTermChange = useCallback( + (searchTerm: string) => { + setSearchParam(searchTerm); + }, + [setSearchParam], + ); + + const handlePageChange = useCallback( + (currentTablePage: number) => { + setPageParam(currentTablePage.toString()); + }, + [setPageParam], + ); + + return [ + { + filterParam, + pageParam, + searchParam, + }, + { + handlePageChange, + handleSearchTermChange, + setFilterParam, + setPageParam, + setSearchParam, + }, + ] as const; +}; + +export default useCustomPagination; diff --git a/ui-next/src/utils/hooks/useEditorForm.ts b/ui-next/src/utils/hooks/useEditorForm.ts new file mode 100644 index 0000000000..bfcb1df574 --- /dev/null +++ b/ui-next/src/utils/hooks/useEditorForm.ts @@ -0,0 +1,94 @@ +import { useCallback, useState } from "react"; +import { FieldValues, UseFormReturn } from "react-hook-form"; +import { + getEditorToFormValue, + getFormToEditorValue, +} from "utils/reactHookForm"; +import { tryToJson } from "utils/utils"; + +export const useEditorForm = < + T extends FieldValues, + TTransformedValues = undefined, +>({ + formMethods, + hiddenKeys = [], +}: { + formMethods: UseFormReturn; + hiddenKeys?: string[]; +}): { + editorValue: string; + isEditorValid: boolean; + setInitialFormData: (value: T) => void; + updateEditorValue: (value?: string) => void; +} => { + const [editorValue, setEditorValue] = useState(""); + const [isEditorValid, setIsEditorValid] = useState(true); + + const updateEditorValue = useCallback( + (value?: string) => { + const editorText = + value ?? getFormToEditorValue(formMethods.getValues(), hiddenKeys); + setEditorValue(editorText); + + const parsedValue = tryToJson(editorText); + if (parsedValue) { + setIsEditorValid(true); + if (value) { + // When user edits in code editor, update form but keep default values + // This marks the form as dirty (expected behavior) + formMethods.reset( + getEditorToFormValue( + formMethods.getValues() || {}, + parsedValue, + hiddenKeys, + ), + { + keepDefaultValues: true, + }, + ); + } + } else { + setIsEditorValid(false); + } + }, + [formMethods, hiddenKeys], + ); + + const setInitialFormData = useCallback( + (value: T) => { + // Normalize the incoming data to match the form's default structure + // Convert undefined fields to null to prevent isDirty issues + const currentValues = formMethods.getValues(); + const normalizedValue: T = { ...value }; + + Object.keys(currentValues).forEach((key) => { + if (normalizedValue[key as keyof T] === undefined) { + (normalizedValue as FieldValues)[key] = null; + } + }); + + // Reset form with normalized data and explicitly update defaultValues + // Use keepValues: false and keepDefaultValues: false to ensure + // the form's internal state is completely reset + formMethods.reset(normalizedValue, { + keepValues: false, + keepDefaultValues: false, + keepErrors: false, + keepDirty: false, + keepIsValid: false, + keepTouched: false, + keepIsSubmitted: false, + keepSubmitCount: false, + }); + updateEditorValue(); + }, + [formMethods, updateEditorValue], + ); + + return { + editorValue, + isEditorValid, + setInitialFormData, + updateEditorValue, + }; +}; diff --git a/ui-next/src/utils/hooks/useEntityAvailableVersions.ts b/ui-next/src/utils/hooks/useEntityAvailableVersions.ts new file mode 100644 index 0000000000..93a3fa59a2 --- /dev/null +++ b/ui-next/src/utils/hooks/useEntityAvailableVersions.ts @@ -0,0 +1,37 @@ +import { useMemo } from "react"; +import _ from "lodash"; +import { useFetch } from "utils"; + +type useEntityAvailableVersionsProps = { + url: string; + name: string; + queryKey?: string[]; +}; + +export const useEntityAvailableVersions = ({ + url, + name, +}: useEntityAvailableVersionsProps) => { + const { data, refetch, isFetching } = useFetch(url); + + const availableVersions: number[] = useMemo(() => { + return ( + _.chain(data) + .groupBy("name") + .map((group, key) => ({ + name: key, + versions: _.map(group, "version"), + })) + .find((item) => item.name === name) + .get("versions") + .orderBy((version) => version, "asc") + .value() || [] + ); + }, [data, name]); + + return { + availableVersions, + refetchAvailableVersions: refetch, + isFetchingAvailableVersions: isFetching, + }; +}; diff --git a/ui-next/src/utils/hooks/useEventNameSuggestions.ts b/ui-next/src/utils/hooks/useEventNameSuggestions.ts new file mode 100644 index 0000000000..380e6c4161 --- /dev/null +++ b/ui-next/src/utils/hooks/useEventNameSuggestions.ts @@ -0,0 +1,14 @@ +import { useMemo } from "react"; +import { useGetIntegration } from "./useGetIntegrations"; +import { MESSAGE_BROKER } from "../constants/event"; + +export const useEventNameSuggestions = () => { + const { data: integrations = [] } = useGetIntegration({ + category: MESSAGE_BROKER, + }); + + return useMemo( + () => integrations.map(({ type, name }) => `${type}:${name}`), + [integrations], + ); +}; diff --git a/ui-next/src/utils/hooks/useGetEntities.ts b/ui-next/src/utils/hooks/useGetEntities.ts new file mode 100644 index 0000000000..093cf60af0 --- /dev/null +++ b/ui-next/src/utils/hooks/useGetEntities.ts @@ -0,0 +1,23 @@ +import { useMemo } from "react"; +import { useFetch } from "utils"; + +type useGetEntitesProps = { + url: string; + map?: (entities: T[]) => U[]; +}; + +export const useGetEntites = ({ url, map }: useGetEntitesProps) => { + const { data } = useFetch(url); + + const entities: U[] = useMemo(() => { + if (!data) { + return []; + } + + return map ? map(data) : data; + }, [data, map]); + + return { + entities, + }; +}; diff --git a/ui-next/src/utils/hooks/useGetEnvironmentVariables.ts b/ui-next/src/utils/hooks/useGetEnvironmentVariables.ts new file mode 100644 index 0000000000..650ac395ea --- /dev/null +++ b/ui-next/src/utils/hooks/useGetEnvironmentVariables.ts @@ -0,0 +1,31 @@ +import { fetchWithContext, useFetchContext } from "plugins/fetch"; +import { useQuery } from "react-query"; +import { EnvironmentVariables } from "types/EnvVariables"; +import { STALE_TIME_SEARCH, useAuthHeaders } from "utils/query"; + +const ENVIRONMENT_VARIABLES_PATH = "/environment"; + +export const useGetEnvironmentVariables = () => { + const fetchContext = useFetchContext(); + const fetchParams = { headers: useAuthHeaders() }; + + return useQuery( + [fetchContext.stack, ENVIRONMENT_VARIABLES_PATH], + () => { + const path = ENVIRONMENT_VARIABLES_PATH; + return fetchWithContext(path, fetchContext, fetchParams); + // staletime to ensure stable view when paginating back and forth (even if underlying results change) + }, + { + enabled: fetchContext.ready, + keepPreviousData: true, + staleTime: STALE_TIME_SEARCH, + retry: (failureCount: number, error: any) => { + if (error?.status >= 400 && error.status < 500) { + return false; + } + return failureCount > 3; + }, + }, + ); +}; diff --git a/ui-next/src/utils/hooks/useGetIntegrations.ts b/ui-next/src/utils/hooks/useGetIntegrations.ts new file mode 100644 index 0000000000..bb58d44574 --- /dev/null +++ b/ui-next/src/utils/hooks/useGetIntegrations.ts @@ -0,0 +1,63 @@ +import { fetchWithContext, useFetchContext } from "plugins/fetch"; +import { + useAuthHeaders, + STALE_TIME_WORKFLOW_DEFS, + toMaybeQueryString, + DEFAULT_STALE_TIME, +} from "utils"; +import { useQuery } from "react-query"; +import { IntegrationDef, IntegrationI } from "types"; + +const INTEGRATIONS_PATH = "/integrations/provider"; +const INTEGRATION_DEF_PATH = "/integrations/def"; + +type GetIntegrationsProps = { + category?: string; + activeOnly?: boolean; +}; + +export const useGetIntegration = ({ + activeOnly = false, + ...restProps +}: GetIntegrationsProps) => { + const fetchContext = useFetchContext(); + const fetchParams = { headers: useAuthHeaders() }; + const props = { activeOnly, ...restProps }; + + return useQuery( + [fetchContext.stack, INTEGRATIONS_PATH, props], + () => { + const path = `${INTEGRATIONS_PATH}${toMaybeQueryString(props)}`; + return fetchWithContext(path, fetchContext, fetchParams); + // staletime to ensure stable view when paginating back and forth (even if underlying results change) + }, + { + enabled: fetchContext.ready, + keepPreviousData: true, + staleTime: DEFAULT_STALE_TIME, + retry: (failureCount: number, error: any) => { + if (error?.status >= 400 && error.status < 500) { + return false; + } + return failureCount > 3; + }, + }, + ); +}; + +export const useGetIntegrationDef = () => { + const fetchContext = useFetchContext(); + const fetchParams = { headers: useAuthHeaders() }; + return useQuery( + [fetchContext.stack, INTEGRATION_DEF_PATH], + () => { + const path = `${INTEGRATION_DEF_PATH}`; + return fetchWithContext(path, fetchContext, fetchParams); + // staletime to ensure stable view when paginating back and forth (even if underlying results change) + }, + { + enabled: fetchContext.ready, + staleTime: STALE_TIME_WORKFLOW_DEFS, + }, + ); +}; diff --git a/ui-next/src/utils/hooks/useGetSchedulerDefinitions.ts b/ui-next/src/utils/hooks/useGetSchedulerDefinitions.ts new file mode 100644 index 0000000000..6bea974bfb --- /dev/null +++ b/ui-next/src/utils/hooks/useGetSchedulerDefinitions.ts @@ -0,0 +1,86 @@ +import { fetchWithContext, useFetchContext } from "plugins/fetch"; +import qs from "qs"; +import { useMemo } from "react"; +import { useQuery } from "react-query"; +import { IScheduleDto, SchedulerSearchResult } from "types/Schedulers"; +import { STALE_TIME_SEARCH, useAuthHeaders } from "utils/query"; + +const SCHEDULER_PATH = "/scheduler/schedules"; +const SCHEDULER_SEARCH_PATH = "/scheduler/schedules/search?"; + +export const useGetSchedulerDefinitions = () => { + const fetchContext = useFetchContext(); + const fetchParams = { headers: useAuthHeaders() }; + + return useQuery( + [fetchContext.stack, SCHEDULER_PATH], + () => { + const path = SCHEDULER_PATH; + return fetchWithContext(path, fetchContext, fetchParams); + // staletime to ensure stable view when paginating back and forth (even if underlying results change) + }, + { + enabled: fetchContext.ready, + keepPreviousData: true, + staleTime: STALE_TIME_SEARCH, + retry: (failureCount: number, error: any) => { + if (error?.status >= 400 && error.status < 500) { + return false; + } + return failureCount > 3; + }, + }, + ); +}; + +export interface SchedulerSearchParams { + start?: number; + size?: number; + sort?: string; + workflowName?: string; + name?: string; + paused?: boolean; +} + +export const useGetSchedulerDefinitionsWithPagination = ( + searchParams: SchedulerSearchParams, +) => { + const fetchContext = useFetchContext(); + const fetchParams = { headers: useAuthHeaders() }; + + return useQuery( + [fetchContext.stack, SCHEDULER_SEARCH_PATH, searchParams], + () => { + const params = { + start: searchParams.start ?? 0, + size: searchParams.size ?? 100, + ...(searchParams.sort && { sort: searchParams.sort }), + ...(searchParams.workflowName && { + workflowName: searchParams.workflowName, + }), + ...(searchParams.name && { freeText: searchParams.name }), + ...(searchParams.paused !== undefined && { + paused: searchParams.paused, + }), + }; + const path = SCHEDULER_SEARCH_PATH + qs.stringify(params); + return fetchWithContext(path, fetchContext, fetchParams); + }, + { + enabled: fetchContext.ready, + keepPreviousData: true, + staleTime: STALE_TIME_SEARCH, + retry: (failureCount: number, error: any) => { + if (error?.status >= 400 && error.status < 500) { + return false; + } + return failureCount > 3; + }, + }, + ); +}; + +export function useScheduleNames() { + const { data } = useGetSchedulerDefinitions(); + return useMemo(() => (data ? data.map((def) => def.name) : []), [data]); +} diff --git a/ui-next/src/utils/hooks/useGetSchemas.ts b/ui-next/src/utils/hooks/useGetSchemas.ts new file mode 100644 index 0000000000..fa232701fb --- /dev/null +++ b/ui-next/src/utils/hooks/useGetSchemas.ts @@ -0,0 +1,31 @@ +import { fetchWithContext, useFetchContext } from "plugins/fetch"; +import { useQuery } from "react-query"; +import { SchemaDefinition } from "types/SchemaDefinition"; +import { DEFAULT_STALE_TIME, useAuthHeaders } from "utils/query"; + +const SCHEMAS_PATH = "/schema"; + +export const useGetSchemas = () => { + const fetchContext = useFetchContext(); + const fetchParams = { headers: useAuthHeaders() }; + + return useQuery( + [fetchContext.stack, SCHEMAS_PATH, {}], + () => { + const path = SCHEMAS_PATH; + return fetchWithContext(path, fetchContext, fetchParams); + // staletime to ensure stable view when paginating back and forth (even if underlying results change) + }, + { + enabled: fetchContext.ready, + keepPreviousData: true, + staleTime: DEFAULT_STALE_TIME, + retry: (failureCount: number, error: any) => { + if (error?.status >= 400 && error.status < 500) { + return false; + } + return failureCount > 3; + }, + }, + ); +}; diff --git a/ui-next/src/utils/hooks/useGetSecrets.ts b/ui-next/src/utils/hooks/useGetSecrets.ts new file mode 100644 index 0000000000..1c60c409a5 --- /dev/null +++ b/ui-next/src/utils/hooks/useGetSecrets.ts @@ -0,0 +1,31 @@ +import { fetchWithContext, useFetchContext } from "plugins/fetch"; +import { useQuery } from "react-query"; +import { SecretDTO } from "types/Secret"; +import { STALE_TIME_SEARCH, useAuthHeaders } from "utils/query"; + +const SECRETS_PATH = "/secrets-v2"; + +export const useGetSecrets = () => { + const fetchContext = useFetchContext(); + const fetchParams = { headers: useAuthHeaders() }; + + return useQuery( + [fetchContext.stack, SECRETS_PATH], + () => { + const path = SECRETS_PATH; + return fetchWithContext(path, fetchContext, fetchParams); + // staletime to ensure stable view when paginating back and forth (even if underlying results change) + }, + { + enabled: fetchContext.ready, + keepPreviousData: true, + staleTime: STALE_TIME_SEARCH, + retry: (failureCount: number, error: any) => { + if (error?.status >= 400 && error.status < 500) { + return false; + } + return failureCount > 3; + }, + }, + ); +}; diff --git a/ui-next/src/utils/hooks/useMCPIntegrations.ts b/ui-next/src/utils/hooks/useMCPIntegrations.ts new file mode 100644 index 0000000000..7766fe2ca1 --- /dev/null +++ b/ui-next/src/utils/hooks/useMCPIntegrations.ts @@ -0,0 +1,57 @@ +import { useMemo } from "react"; +import { useFetch } from "utils/query"; + +export const useMCPIntegrations = () => { + const integrationsUrl = `/integrations/def`; + const providersUrl = `/integrations/provider?category=MCP&activeOnly=false`; + + const { data: integrationsData, isLoading: isLoadingIntegrations } = + useFetch(integrationsUrl); + const { data: providersData, isLoading: isLoadingProviders } = + useFetch(providersUrl); + + const combinedIntegrations = useMemo(() => { + if (!providersData) return []; + + const supportedIntegrations = + integrationsData?.filter( + (integration: any) => integration.category === "MCP", + ) || []; + + const availableIntegrations = + providersData?.filter((provider: any) => provider.category === "MCP") || + []; + + // Combine both arrays with status information + const combined = [ + ...availableIntegrations.map((integration: any) => ({ + ...integration, + status: "active" as const, + iconName: supportedIntegrations.find( + (supportedIntegration: any) => + supportedIntegration.type === integration.type, + )?.iconName, + })), + ]; + + return combined; + }, [integrationsData, providersData]); + + return { + integrations: combinedIntegrations, + isLoading: isLoadingIntegrations || isLoadingProviders, + }; +}; + +export const useMCPTools = (integrationName?: string) => { + const toolsUrl = integrationName + ? `/integrations/${integrationName}/def/apis` + : null; + + const { data: tools, isLoading } = useFetch(toolsUrl || ""); + + return { + tools: tools || [], + isLoading: isLoading, + }; +}; diff --git a/ui-next/src/utils/hooks/usePushHistory.ts b/ui-next/src/utils/hooks/usePushHistory.ts new file mode 100644 index 0000000000..90476bd115 --- /dev/null +++ b/ui-next/src/utils/hooks/usePushHistory.ts @@ -0,0 +1,17 @@ +import { useEnv } from "plugins/env"; +import { useNavigate } from "react-router"; +import Url from "url-parse"; + +export function usePushHistory() { + const navigate = useNavigate(); + const { stack, defaultStack } = useEnv(); + + return (path: string) => { + const url = new Url(path, {}, true); + if (stack !== defaultStack) { + url.query.stack = stack; + } + + navigate(url.toString()); + }; +} diff --git a/ui-next/src/utils/hooks/useReplaceHistory.ts b/ui-next/src/utils/hooks/useReplaceHistory.ts new file mode 100644 index 0000000000..6a323e1913 --- /dev/null +++ b/ui-next/src/utils/hooks/useReplaceHistory.ts @@ -0,0 +1,17 @@ +import { useEnv } from "plugins/env"; +import { useNavigate } from "react-router"; +import Url from "url-parse"; + +export function useReplaceHistory() { + const navigate = useNavigate(); + const { stack, defaultStack } = useEnv(); + + return (path: string) => { + const url = new Url(path, {}, true); + if (stack !== defaultStack) { + url.query.stack = stack; + } + + navigate(url.toString(), { replace: true }); + }; +} diff --git a/ui-next/src/utils/hooks/useToastMessage.ts b/ui-next/src/utils/hooks/useToastMessage.ts new file mode 100644 index 0000000000..20981a3f29 --- /dev/null +++ b/ui-next/src/utils/hooks/useToastMessage.ts @@ -0,0 +1,21 @@ +import { MessageContext } from "components/v1/layout/MessageContext"; +import { useCallback, useContext } from "react"; +import { PopoverMessage } from "types/Messages"; + +export const useToastMessage = () => { + const { setMessage } = useContext(MessageContext); + + const toastMessage = useCallback( + ({ text, severity }: PopoverMessage) => { + setMessage({ + text, + severity, + }); + }, + [setMessage], + ); + + return { + toastMessage, + }; +}; diff --git a/ui-next/src/utils/hooks/useWorkflowNamesAndVersionsQuery.ts b/ui-next/src/utils/hooks/useWorkflowNamesAndVersionsQuery.ts new file mode 100644 index 0000000000..c25417c440 --- /dev/null +++ b/ui-next/src/utils/hooks/useWorkflowNamesAndVersionsQuery.ts @@ -0,0 +1,25 @@ +import { useMemo } from "react"; +import { + useSharedQueryContext, + useFetch, + STALE_TIME_WORKFLOW_DEFS, +} from "../query"; +import { getUniqueWorkflowsWithVersions } from "../workflow"; + +export function useWorkflowNamesAndVersionsQuery(): [ + Map, + ReturnType, +] { + const { url } = useSharedQueryContext(); + const fetchResult = useFetch(url, { + staleTime: STALE_TIME_WORKFLOW_DEFS, + }); + + return [ + useMemo( + () => getUniqueWorkflowsWithVersions(fetchResult.data), + [fetchResult.data], + ), + fetchResult, + ]; +} diff --git a/ui-next/src/utils/hooks/useXStateEventListener.ts b/ui-next/src/utils/hooks/useXStateEventListener.ts new file mode 100644 index 0000000000..11bc8df6e5 --- /dev/null +++ b/ui-next/src/utils/hooks/useXStateEventListener.ts @@ -0,0 +1,22 @@ +import { useEffect } from "react"; +import { EventObject, ActorRef, State } from "xstate"; + +function useXStateEventListener( + actorRef: ActorRef>, + eventType: TEvent["type"], + callback: (event: TEvent) => void, +) { + useEffect(() => { + const subscription = actorRef.subscribe((state) => { + if (state.event.type === eventType) { + callback(state.event); + } + }); + + return () => { + subscription.unsubscribe(); + }; + }, [actorRef, eventType, callback]); +} + +export default useXStateEventListener; diff --git a/ui-next/src/utils/httpStatus.ts b/ui-next/src/utils/httpStatus.ts new file mode 100644 index 0000000000..ac0bf94c4e --- /dev/null +++ b/ui-next/src/utils/httpStatus.ts @@ -0,0 +1,13 @@ +export function getHttpStatusText(code: string): string { + const statusCodes: Record = { + "400": "Bad Request", + "401": "Unauthorized", + "403": "Forbidden", + "404": "Not Found", + "500": "Internal Server Error", + "502": "Bad Gateway", + "503": "Service Unavailable", + "504": "Gateway Timeout", + }; + return statusCodes[code] || "Unknown Error"; +} diff --git a/ui-next/src/utils/human.ts b/ui-next/src/utils/human.ts new file mode 100644 index 0000000000..97509a1fa6 --- /dev/null +++ b/ui-next/src/utils/human.ts @@ -0,0 +1,100 @@ +import { JsonSchema, ControlElement } from "@jsonforms/core"; +import _path from "lodash/fp/path"; +import _last from "lodash/last"; +import _isEmpty from "lodash/isEmpty"; +import { HumanTemplate } from "types/HumanTaskTypes"; +import { logger } from "utils"; + +type TypeAndFieldName = { type: string; fieldName: string; path: string }; + +export const extractFieldTypeAndName = ( + jsonSchema: JsonSchema, + uiTemplate: ControlElement, +): TypeAndFieldName | undefined => { + const path = uiTemplate.scope.substring(2).replaceAll("/", "."); + const fieldName = _last(path.split(".")); + const type: string = _path(path + ".type", jsonSchema); + if ([type, fieldName, path].some((a) => a == null)) { + return undefined; + } + + return { + type, + fieldName: fieldName!, + path, + }; +}; + +export const enumValuesForField = (path: string, jsonSchema: JsonSchema) => + _path(`${path}.enum`, jsonSchema); + +type TemplateByNameRow = Record; +type TemplateById = Record; + +export const groupedByTemplates = ( + templates: HumanTemplate[], +): [TemplateByNameRow, TemplateById] => { + if (_isEmpty(templates)) return [{}, {}]; + const [templateByName, templateByIdAcc]: [TemplateByNameRow, TemplateById] = + templates.reduce( + ( + accL: [TemplateByNameRow, TemplateById], + template: HumanTemplate, + ): [TemplateByNameRow, TemplateById] => { + const templateByNameAcc = accL[0]; + const templateByIdAcc = accL[1]; + if (!templateByNameAcc[template.name]) { + templateByNameAcc[template.name] = []; + } + templateByNameAcc[template.name].push(template); + templateByIdAcc[template.name] = template; + return [templateByNameAcc, templateByIdAcc]; + }, + [{}, {}], + ); + return [templateByName, templateByIdAcc]; +}; + +export const templatesToGroupedSingleTemplates = ( + templates: HumanTemplate[], +): [HumanTemplate[], TemplateByNameRow, TemplateById] => { + if (_isEmpty(templates)) return [[], {}, {}]; + const [templateByName, templateByIdAcc]: [TemplateByNameRow, TemplateById] = + groupedByTemplates(templates); + + const dataWithLatestVersion = Object.entries(templateByName).map( + ([, versions]: [string, HumanTemplate[]]) => + versions.reduce((acc: HumanTemplate, curr: HumanTemplate) => { + return acc?.version > curr.version ? acc : curr; + }), + ); + return [dataWithLatestVersion, templateByName, templateByIdAcc]; +}; + +const defaultValueForType = (type: string) => { + switch (type) { + case "number": + case "integer": + return 0; + case "boolean": + return false; + default: + return ""; + } +}; + +export const extractTemplatePropertiesSetDefaultValues = ( + humanTemplate: HumanTemplate | undefined, +): Record => { + if (humanTemplate?.jsonSchema?.properties !== undefined) { + const { jsonSchema } = humanTemplate; + return Object.fromEntries( + Object.entries(jsonSchema?.properties || {}).map(([key, value]) => [ + key, + defaultValueForType(value.type), + ]), + ); + } + logger.info("No properties found in template"); + return {}; +}; diff --git a/ui-next/src/utils/index.ts b/ui-next/src/utils/index.ts new file mode 100644 index 0000000000..2daf71cfd9 --- /dev/null +++ b/ui-next/src/utils/index.ts @@ -0,0 +1,21 @@ +export * from "./array"; +export * from "./date"; +export * from "./flags"; +export * from "./gtag"; +export * from "./handleValidChars"; +export * from "./helpers"; +export * from "./localstorage"; +export * from "./logger"; +export * from "./logrocket"; +export * from "./object"; +export * from "./query"; +export * from "./releaseVersion"; +export * from "./roles"; +export * from "./strings"; +export * from "./task"; +export * from "./toMaybeQueryString"; +export * from "./tracker"; +export * from "./useGetGroups"; +export * from "./useGetUsers"; +export * from "./utils"; +export * from "./workflow"; diff --git a/ui-next/src/utils/json.ts b/ui-next/src/utils/json.ts new file mode 100644 index 0000000000..f2103baac4 --- /dev/null +++ b/ui-next/src/utils/json.ts @@ -0,0 +1,586 @@ +import cloneDeep from "lodash/cloneDeep"; + +const VARIABLE_REGEX = /\$\{([^}]+)\}/g; + +export const extractVariablesFromJSON = (data: Record) => { + const extractedVariables: Record = {}; + + const processObject = (obj: Record, path = ""): void => { + for (const [key, value] of Object.entries(obj)) { + if (typeof value === "string") { + let match; + while ((match = VARIABLE_REGEX.exec(value)) !== null) { + const variableName = match[1]; + const keyName = + path !== "" ? `${path.replace(/^\./, "")}.${key}` : key; + extractedVariables[keyName] = variableName; + } + } else if (typeof value === "object" && value !== null) { + processObject(value as Record, `${path}.${key}`); + } + } + }; + + processObject(data); + + return extractedVariables; +}; + +/** + * Downgrades a JSON schema from newer versions (Draft 2019-09, Draft 2020-12) to Draft 7. + * This function: + * - Replaces $schema URI with Draft 7 URI + * - Converts $defs to definitions + * - Removes unsupported keywords (unevaluatedProperties, unevaluatedItems, etc.) + * + * @param schema - The JSON schema to downgrade + * @returns A downgraded schema compatible with Draft 7, or an empty object if input is invalid + */ +export const downgradeSchemaToDraft7 = ( + schema: Record, +): Record => { + // Defensive check: handle null, undefined, non-objects, and arrays + if (schema == null || typeof schema !== "object" || Array.isArray(schema)) { + // Return empty object for invalid inputs to maintain type safety + if (schema == null || typeof schema !== "object") { + return {}; + } + // Return as-is for arrays (they might be valid in some contexts, but not as root schema) + return schema; + } + + // Recursively check nested schema objects + const nestedKeys = [ + "properties", + "items", + "additionalProperties", + "patternProperties", + "allOf", + "anyOf", + "oneOf", + "not", + "if", + "then", + "else", + "definitions", + "$defs", + ] as const; + + const NEWER_KEYWORDS = new Set([ + "$defs", + "unevaluatedProperties", + "unevaluatedItems", + "dependentRequired", + "dependentSchemas", + "$anchor", + "$dynamicAnchor", + "$dynamicRef", + "minContains", + "maxContains", + ] as const); + + // Recursively check if schema has newer keywords anywhere (including nested) + const hasNewerKeywordsRecursive = (obj: any): boolean => { + // Defensive checks: handle null, undefined, non-objects + if (obj == null || typeof obj !== "object") { + return false; + } + + // Handle arrays with defensive checks + if (Array.isArray(obj)) { + // Check for empty arrays + if (obj.length === 0) { + return false; + } + // Safely iterate through array items + try { + return obj.some((item) => { + try { + return hasNewerKeywordsRecursive(item); + } catch { + // If processing an item fails, continue checking other items + return false; + } + }); + } catch { + // If array iteration fails, assume no newer keywords + return false; + } + } + + // Check for newer keywords at current level + try { + for (const key of NEWER_KEYWORDS) { + if (obj != null && key in obj) { + return true; + } + } + } catch { + // If keyword checking fails, continue to nested checks + } + + // Check nested schema objects + try { + for (const key of nestedKeys) { + if (obj == null || !(key in obj)) { + continue; + } + + const value = obj[key]; + + // Skip if value is null, undefined, or empty string + if (value == null || value === "") { + continue; + } + + if (Array.isArray(value)) { + // Handle empty arrays + if (value.length === 0) { + continue; + } + // Safely check array items + try { + if (value.some((item) => hasNewerKeywordsRecursive(item))) { + return true; + } + } catch { + // Continue checking other nested keys if this fails + continue; + } + } else if (typeof value === "object" && value !== null) { + if ( + key === "properties" || + key === "patternProperties" || + key === "definitions" || + key === "$defs" + ) { + // These are objects with schema values + // Defensive check: ensure value is a proper object + if ( + value == null || + typeof value !== "object" || + Array.isArray(value) + ) { + continue; + } + + try { + for (const nestedKey in value) { + // Check if property exists and is own property + if (!Object.prototype.hasOwnProperty.call(value, nestedKey)) { + continue; + } + + const nestedValue = value[nestedKey]; + // Skip null/undefined nested values + if (nestedValue == null) { + continue; + } + + if (hasNewerKeywordsRecursive(nestedValue)) { + return true; + } + } + } catch { + // If iteration fails, continue checking other keys + continue; + } + } else { + // Recursively check other object values + try { + if (hasNewerKeywordsRecursive(value)) { + return true; + } + } catch { + // Continue if recursive check fails + continue; + } + } + } + } + } catch { + // If nested checking fails, assume no newer keywords + return false; + } + + return false; + }; + + // Check if schema version needs conversion to Draft 7 + // JsonForms only supports Draft 7, so we need to convert Draft 04, 06, and newer versions + // Also ensure $schema is always set to Draft 7 for JsonForms compatibility + const schemaVersion = schema?.$schema; + const isDraft07 = + schemaVersion != null && + typeof schemaVersion === "string" && + schemaVersion.length > 0 && + schemaVersion.includes("draft-07"); + + // Check if schema has HTTPS URI which needs to be converted to HTTP + // Ajv tries to fetch HTTPS URIs, causing errors + const hasHttpsSchemaUri = + schemaVersion != null && + typeof schemaVersion === "string" && + schemaVersion.startsWith("https://"); + + // Check if downgrading/conversion is necessary before cloning + try { + // Only return early if already Draft 7 with HTTP (not HTTPS) URI and no newer keywords + // If $schema is missing or is HTTPS, we need to process it + if (isDraft07 && !hasHttpsSchemaUri && !hasNewerKeywordsRecursive(schema)) { + // Already Draft 7 with HTTP URI and no newer keywords to convert anywhere + return schema; + } + } catch { + // If checking fails, proceed with downgrading to be safe + } + + // Create a deep copy to avoid mutating the original (only if downgrading is needed) + // Try structuredClone first (native browser API), fallback to lodash cloneDeep + let downgraded: Record; + try { + downgraded = + typeof structuredClone !== "undefined" + ? structuredClone(schema) + : cloneDeep(schema); + + // Defensive check: ensure cloning succeeded + if ( + downgraded == null || + typeof downgraded !== "object" || + Array.isArray(downgraded) + ) { + // If cloning failed, return original schema or empty object + return schema != null && + typeof schema === "object" && + !Array.isArray(schema) + ? schema + : {}; + } + } catch { + // If cloning fails (e.g., circular references), return original schema + // or empty object as fallback + return schema != null && + typeof schema === "object" && + !Array.isArray(schema) + ? schema + : {}; + } + + // Replace $schema with Draft 7 URI (without HTTPS to avoid external fetch issues) + // JsonForms/Ajv will try to fetch HTTPS URIs, causing errors + try { + // Use HTTP instead of HTTPS to prevent Ajv from trying to fetch the schema + // Both URIs are equivalent for JSON Schema Draft 7, but HTTP prevents validation errors + downgraded.$schema = "http://json-schema.org/draft-07/schema#"; + } catch { + // If setting $schema fails, continue processing + } + + // Recursively process the schema + const processSchema = (objA: any): any => { + // Defensive check: handle null, undefined, and non-objects + if (objA == null) { + return objA; + } + + if (typeof objA !== "object") { + return objA; + } + + // Handle arrays - process each element + if (Array.isArray(objA)) { + // Handle empty arrays + if (objA.length === 0) { + return objA; + } + + try { + return objA.map((item) => { + try { + return processSchema(item); + } catch { + // If processing an item fails, return it as-is + return item; + } + }); + } catch { + // If mapping fails, return array as-is + return objA; + } + } + + // Create a shallow copy for processing + let obj: Record; + try { + obj = { ...objA }; + } catch { + // If spreading fails, use original object + obj = objA; + } + + // Defensive check: ensure obj is still a valid object + if (obj == null || typeof obj !== "object" || Array.isArray(obj)) { + return obj; + } + + // Convert $defs to definitions + let definitionsProcessed = false; + try { + if ( + obj.$defs != null && + typeof obj.$defs === "object" && + !Array.isArray(obj.$defs) + ) { + // Merge $defs into existing definitions if it exists, otherwise create it + if ( + obj.definitions != null && + typeof obj.definitions === "object" && + !Array.isArray(obj.definitions) + ) { + // Merge $defs into definitions, with $defs taking precedence for duplicate keys + try { + obj.definitions = { ...obj.definitions, ...obj.$defs }; + } catch { + // If merging fails, just use $defs + obj.definitions = obj.$defs; + } + } else { + obj.definitions = obj.$defs; + } + + delete obj.$defs; + + // Recursively process definitions + if ( + obj.definitions != null && + typeof obj.definitions === "object" && + !Array.isArray(obj.definitions) + ) { + try { + for (const key in obj.definitions) { + if (Object.prototype.hasOwnProperty.call(obj.definitions, key)) { + const defValue = obj.definitions[key]; + if (defValue != null) { + obj.definitions[key] = processSchema(defValue); + } + } + } + definitionsProcessed = true; + } catch { + // If processing definitions fails, continue + definitionsProcessed = true; + } + } + } + } catch { + // If $defs processing fails, continue with other processing + } + + // Update $ref values that reference $defs to use definitions instead + try { + if ( + obj.$ref != null && + typeof obj.$ref === "string" && + obj.$ref.length > 0 + ) { + // Replace #/$defs/ with #/definitions/ + obj.$ref = obj.$ref.replace(/^#\/\$defs\//, "#/definitions/"); + + // Remove external HTTP(S) references that JsonForms can't resolve + // This fixes the error: "no schema with key or ref https://json-schema.org/draft-07/schema#" + if (obj.$ref.startsWith("http://") || obj.$ref.startsWith("https://")) { + delete obj.$ref; + } + } + } catch { + // If $ref processing fails, continue + } + + // Remove unsupported keywords + const unsupportedKeywords = [ + "unevaluatedProperties", + "unevaluatedItems", + "dependentRequired", + "dependentSchemas", + "$anchor", + "$dynamicAnchor", + "$dynamicRef", + "minContains", + "maxContains", + ] as const; + + try { + for (const keyword of unsupportedKeywords) { + if (obj != null && keyword in obj) { + try { + delete obj[keyword]; + } catch { + // If deletion fails, continue with other keywords + continue; + } + } + } + } catch { + // If keyword removal fails, continue with processing + } + + // Process nested schema objects + // Configuration for different schema key processing strategies + const schemaProcessors: Record< + string, + { + type: "object" | "array" | "arrayOrSingle" | "single"; + condition?: (value: any) => boolean; + } + > = { + properties: { type: "object" }, + patternProperties: { type: "object" }, + items: { type: "arrayOrSingle" }, + additionalProperties: { + type: "single", + condition: (value) => + value != null && typeof value === "object" && !Array.isArray(value), + }, + allOf: { type: "array" }, + anyOf: { type: "array" }, + oneOf: { type: "array" }, + not: { type: "single" }, + if: { type: "single" }, + then: { type: "single" }, + else: { type: "single" }, + definitions: { + type: "object", + condition: () => !definitionsProcessed, + }, + }; + + try { + for (const [key, processor] of Object.entries(schemaProcessors)) { + if (obj == null) { + break; + } + + const value = obj[key]; + + // Skip null, undefined, and empty strings + if (value === undefined || value === null || value === "") { + continue; + } + + // Check condition if provided + try { + if (processor.condition && !processor.condition(value)) { + continue; + } + } catch { + // If condition check fails, skip this processor + continue; + } + + try { + switch (processor.type) { + case "object": + // Process object with schema values (properties, patternProperties, definitions) + if ( + value != null && + typeof value === "object" && + !Array.isArray(value) + ) { + try { + for (const nestedKey in value) { + if ( + Object.prototype.hasOwnProperty.call(value, nestedKey) + ) { + const nestedValue = value[nestedKey]; + if (nestedValue != null) { + value[nestedKey] = processSchema(nestedValue); + } + } + } + } catch { + // If processing object properties fails, continue + } + } + break; + + case "array": + // Process array of schemas + if (Array.isArray(value)) { + if (value.length > 0) { + try { + obj[key] = value.map((item: any) => { + try { + return processSchema(item); + } catch { + return item; + } + }); + } catch { + // If mapping fails, leave array as-is + } + } + } + break; + + case "arrayOrSingle": + // Process items which can be array or single schema + if (Array.isArray(value)) { + if (value.length > 0) { + try { + obj[key] = value.map((item: any) => { + try { + return processSchema(item); + } catch { + return item; + } + }); + } catch { + // If mapping fails, leave array as-is + } + } + } else if (value != null) { + try { + obj[key] = processSchema(value); + } catch { + // If processing fails, leave value as-is + } + } + break; + + case "single": + // Process single schema value + if (value != null) { + try { + obj[key] = processSchema(value); + } catch { + // If processing fails, leave value as-is + } + } + break; + } + } catch { + // If processing this key fails, continue with other keys + continue; + } + } + } catch { + // If schema processing fails, return what we have so far + } + + return obj; + }; + + try { + return processSchema(downgraded); + } catch { + // If final processing fails, return the cloned schema or original as fallback + return downgraded != null && + typeof downgraded === "object" && + !Array.isArray(downgraded) + ? downgraded + : schema != null && typeof schema === "object" && !Array.isArray(schema) + ? schema + : {}; + } +}; diff --git a/ui-next/src/utils/jsonSchema.ts b/ui-next/src/utils/jsonSchema.ts new file mode 100644 index 0000000000..b3207223e6 --- /dev/null +++ b/ui-next/src/utils/jsonSchema.ts @@ -0,0 +1,26 @@ +import { JsonSchema } from "@jsonforms/core"; +import Ajv from "ajv"; +import addFormats from "ajv-formats"; + +const ajv = new Ajv(); +addFormats(ajv); + +/** + * Validates that a given JSON Schema object is a valid draft-07 schema + * that can be used with JsonForms. + * + * @returns true if valid, or an error message string if invalid/missing. + */ +export const isJSONSchemaValid = ( + jsonSchema: JsonSchema | undefined, +): boolean | string => { + if (!jsonSchema) { + return false; + } + try { + ajv.validateSchema(jsonSchema, true); + return true; + } catch (e: any) { + return e.message; + } +}; diff --git a/ui-next/src/utils/localstorage.ts b/ui-next/src/utils/localstorage.ts new file mode 100644 index 0000000000..b5e0761c67 --- /dev/null +++ b/ui-next/src/utils/localstorage.ts @@ -0,0 +1,49 @@ +import { useState } from "react"; +import { logger } from "./logger"; + +const optionalArg = { + parse: JSON.parse, + code: JSON.stringify, +}; + +// If key is null/undefined, hook behaves exactly like useState +export function useLocalStorage( + key: string, + initialValue: unknown, + c = optionalArg, +) { + const initialString = JSON.stringify(initialValue); + + const [storedValue, setStoredValue] = useState(() => { + try { + if (key) { + const item = window.localStorage.getItem(key); + return item ? c.parse(item) : initialValue; + } else { + return initialValue; + } + } catch { + logger.error("Cant read value from local storage"); + return initialValue; + } + }); + + const setValue = (value: unknown) => { + // Allow value to be a function so we have same API as useState + const valueToStore = value instanceof Function ? value(storedValue) : value; + + // Save state + setStoredValue(valueToStore); + + if (key) { + const stringToStore = c.code(valueToStore); + if (stringToStore === initialString) { + window.localStorage.removeItem(key); + } else { + window.localStorage.setItem(key, stringToStore); + } + } + }; + + return [storedValue, setValue]; +} diff --git a/ui-next/src/utils/logger.ts b/ui-next/src/utils/logger.ts new file mode 100644 index 0000000000..fc3c8d9972 --- /dev/null +++ b/ui-next/src/utils/logger.ts @@ -0,0 +1,53 @@ +import { flipObject } from "./object"; + +// This util means to replace console.log +type LogFunction = typeof console.log; + +interface Logger { + debug: LogFunction; + info: LogFunction; + log: LogFunction; + warn: LogFunction; + error: LogFunction; +} + +enum LogLevels { + debug = "debug", + info = "info", + log = "log", + warn = "warn", + error = "error", +} + +// Defines the oder +const LEVEL_ARRAY = [ + LogLevels.debug, + LogLevels.info, + LogLevels.log, + LogLevels.warn, + LogLevels.error, +]; + +const arrayAsObject: Record = Object.assign({}, LEVEL_ARRAY); +const LevelOrderObject = flipObject(arrayAsObject); + +const MIN_LOG_LEVEL_IDX = + LevelOrderObject[ + process.env.NODE_ENV === "development" ? LogLevels.debug : LogLevels.warn + ]; + +const log = (level: LogLevels) => { + const levelIdx = LevelOrderObject[level]; + return (...params: unknown[]) => { + if (levelIdx >= MIN_LOG_LEVEL_IDX) { + console[level](...params); + } + }; +}; + +const logger: Logger = LEVEL_ARRAY.reduce( + (acc, curLevel) => ({ ...acc, [curLevel]: log(curLevel) }), + {}, +) as Logger; + +export { logger }; diff --git a/ui-next/src/utils/logrocket.ts b/ui-next/src/utils/logrocket.ts new file mode 100644 index 0000000000..85ff28a847 --- /dev/null +++ b/ui-next/src/utils/logrocket.ts @@ -0,0 +1,65 @@ +/** + * LogRocket - OSS Stub + * + * LogRocket is an enterprise-only feature. + * This file provides no-op implementations for OSS builds. + * The enterprise package has the full implementation with actual LogRocket integration. + */ + +// LogRocket is never enabled in OSS +export const isLogRocketEnabled = () => false; + +type LogRocketEvents = + | "user_complete_task" + | "user_claim_task" + | "template_import" + | "user_copy_install_script" + | "user_toggle_show_description" + | "user_created_access_key_in_metadata_banner" + | "user_first_workflow_executed" + | "blank_slate_docs_link_clicked" + | "user_recreated_access_key_in_worker_manual_install_instructions" + | "user_recreated_access_key_in_worker_orkes_cli_install_instructions"; + +// No-op: LogRocket tracking disabled in OSS +export const logrocketTrackIfEnabled = ( + _eventName: LogRocketEvents, + _eventProperties?: any, +) => { + // No-op in OSS +}; + +// No-op: LogRocket initialization disabled in OSS +export const useMaybeEnableLogRocket = () => { + // No-op in OSS +}; + +type ICaptureOptions = { + tags?: { + [tagName: string]: string | number | boolean; + }; + extra?: { + [tagName: string]: string | number | boolean; + }; +}; + +// No-op: LogRocket error reporting disabled in OSS +export const reportErrorToLogRocket = ( + _error: Error | string, + _metadata?: ICaptureOptions, +) => { + // No-op in OSS +}; + +type SimpleUserInfo = { + uuid?: string; + user?: any; + id?: string; +}; + +// No-op: LogRocket user identification disabled in OSS +export const useIdentifyUserInLogRocket = ( + _currentUserInfo?: SimpleUserInfo, +) => { + // No-op in OSS +}; diff --git a/ui-next/src/utils/maybeTriggerWorkflow.ts b/ui-next/src/utils/maybeTriggerWorkflow.ts new file mode 100644 index 0000000000..e0ce5e1052 --- /dev/null +++ b/ui-next/src/utils/maybeTriggerWorkflow.ts @@ -0,0 +1,9 @@ +import { toMaybeQueryString } from "./toMaybeQueryString"; +import { featureFlags, FEATURES } from "utils/flags"; + +export const maybeTriggerFailureWorkflow = () => + toMaybeQueryString( + featureFlags.isEnabled(FEATURES.TRIGGER_WORKFLOW) + ? { triggerFailureWorkflow: true } + : {}, + ); diff --git a/ui-next/src/utils/monacoUtils/CodeEditorUtils.ts b/ui-next/src/utils/monacoUtils/CodeEditorUtils.ts new file mode 100644 index 0000000000..3db6c5e2bc --- /dev/null +++ b/ui-next/src/utils/monacoUtils/CodeEditorUtils.ts @@ -0,0 +1,43 @@ +import { + workflowDefinitionSchemaWithDeps, + workflowSchema, +} from "types/Schemas"; +import _initial from "lodash/initial"; +// @ts-ignore +import { registerJQLanguageDefinition } from "monaco-languages-jq"; +import { registerPromQlLangauge } from "./promql"; + +export const JSON_FILE_NAME = "file:///main.json"; +export const JSON_FILE_TASK_NAME = "file:///mainTask.json"; + +export function configureMonaco(monaco: any) { + const modelUri = monaco.Uri.parse(JSON_FILE_NAME); + + const workflowSchemaUri = { + uri: workflowSchema.$id, + fileMatch: [modelUri.toString()], // associate with our model + schema: workflowSchema, + }; + + const schemasWithURI = _initial(workflowDefinitionSchemaWithDeps).map( + (original) => ({ + uri: original.$id, + schema: original, + }), + ); + // @ts-ignore + const result = [workflowSchemaUri].concat(schemasWithURI); + + monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ + validate: true, + schemas: result, + }); +} + +export const configurePromQl = (monaco: any) => { + registerPromQlLangauge(monaco); +}; + +export const configureJQLanguage = (monaco: any) => { + registerJQLanguageDefinition(monaco); +}; diff --git a/ui-next/src/utils/monacoUtils/promql.ts b/ui-next/src/utils/monacoUtils/promql.ts new file mode 100644 index 0000000000..9c9ae23216 --- /dev/null +++ b/ui-next/src/utils/monacoUtils/promql.ts @@ -0,0 +1,319 @@ +// noinspection JSUnusedGlobalSymbols +const languageConfiguration = { + // the default separators except `@$` + // eslint-disable-next-line + wordPattern: /(-?\d*\.\d\w*)|([^`~!#%^&*()\-=+\[{\]}\\|;:'",.<>\/?\s]+)/g, + // Not possible to make comments in PromQL syntax + comments: { + lineComment: "#", + }, + brackets: [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + ], + autoClosingPairs: [ + { open: "{", close: "}" }, + { open: "[", close: "]" }, + { open: "(", close: ")" }, + { open: '"', close: '"' }, + { open: "'", close: "'" }, + ], + surroundingPairs: [ + { open: "{", close: "}" }, + { open: "[", close: "]" }, + { open: "(", close: ")" }, + { open: '"', close: '"' }, + { open: "'", close: "'" }, + { open: "<", close: ">" }, + ], + folding: {}, +}; + +// PromQL Aggregation Operators +// (https://prometheus.io/docs/prometheus/latest/querying/operators/#aggregation-operators) +const aggregations = [ + "sum", + "min", + "max", + "avg", + "group", + "stddev", + "stdvar", + "count", + "count_values", + "bottomk", + "topk", + "quantile", +]; + +// PromQL functions +// (https://prometheus.io/docs/prometheus/latest/querying/functions/) +const functions = [ + "abs", + "absent", + "ceil", + "changes", + "clamp_max", + "clamp_min", + "day_of_month", + "day_of_week", + "days_in_month", + "delta", + "deriv", + "exp", + "floor", + "histogram_quantile", + "holt_winters", + "hour", + "idelta", + "increase", + "irate", + "label_join", + "label_replace", + "ln", + "log2", + "log10", + "minute", + "month", + "predict_linear", + "rate", + "resets", + "round", + "scalar", + "sort", + "sort_desc", + "sqrt", + "time", + "timestamp", + "vector", + "year", +]; + +// PromQL specific functions: Aggregations over time +// (https://prometheus.io/docs/prometheus/latest/querying/functions/#aggregation_over_time) +const aggregationsOverTime = []; +for (const agg of aggregations) { + aggregationsOverTime.push(agg + "_over_time"); +} + +// PromQL vector matching + the by and without clauses +// (https://prometheus.io/docs/prometheus/latest/querying/operators/#vector-matching) +const vectorMatching = [ + "on", + "ignoring", + "group_right", + "group_left", + "by", + "without", +]; +// Produce a regex matching elements : (elt1|elt2|...) +const vectorMatchingRegex = `(${vectorMatching.reduce( + (prev, curr) => `${prev}|${curr}`, +)})`; + +// PromQL Operators +// (https://prometheus.io/docs/prometheus/latest/querying/operators/) +const operators = [ + "+", + "-", + "*", + "/", + "%", + "^", + "==", + "!=", + ">", + "<", + ">=", + "<=", + "and", + "or", + "unless", +]; + +// PromQL offset modifier +// (https://prometheus.io/docs/prometheus/latest/querying/basics/#offset-modifier) +const offsetModifier = ["offset"]; + +// Merging all the keywords in one list +const keywords = aggregations + .concat(functions) + .concat(aggregationsOverTime) + .concat(vectorMatching) + .concat(offsetModifier); + +// noinspection JSUnusedGlobalSymbols +const language = { + ignoreCase: false, + defaultToken: "", + tokenPostfix: ".promql", + + keywords: keywords, + + operators: operators, + vectorMatching: vectorMatchingRegex, + + // we include these common regular expressions + + // eslint-disable-next-line + symbols: /[=>](?!@symbols)/, "@brackets"], + [ + /@symbols/, + { + cases: { + "@operators": "delimiter", + "@default": "", + }, + }, + ], + + // numbers + [/\d+[smhdwy]/, "number"], // 24h, 5m are often encountered in prometheus + + // eslint-disable-next-line + [/\d*\d+[eE]([\-+]?\d+)?(@floatsuffix)/, "number.float"], + + // eslint-disable-next-line + [/\d*\.\d+([eE][\-+]?\d+)?(@floatsuffix)/, "number.float"], + [/0[xX][0-9a-fA-F']*[0-9a-fA-F](@integersuffix)/, "number.hex"], + [/0[0-7']*[0-7](@integersuffix)/, "number.octal"], + [/0[bB][0-1']*[0-1](@integersuffix)/, "number.binary"], + [/\d[\d']*\d(@integersuffix)/, "number"], + [/\d(@integersuffix)/, "number"], + ], + + string_double: [ + [/[^\\"]+/, "string"], + [/@escapes/, "string.escape"], + [/\\./, "string.escape.invalid"], + [/"/, "string", "@pop"], + ], + + string_single: [ + [/[^\\']+/, "string"], + [/@escapes/, "string.escape"], + [/\\./, "string.escape.invalid"], + [/'/, "string", "@pop"], + ], + + string_backtick: [ + [/[^\\`$]+/, "string"], + [/@escapes/, "string.escape"], + [/\\./, "string.escape.invalid"], + [/`/, "string", "@pop"], + ], + + clauses: [ + [/[^(,)]/, "tag"], + [/\)/, "identifier", "@pop"], + ], + + whitespace: [[/[ \t\r\n]+/, "white"]], + }, +}; + +// noinspection JSUnusedGlobalSymbols +const loadLanguage = (monaco: any) => ({ + id: "promql", + extensions: [".promql"], + aliases: [ + "Prometheus", + "prometheus", + "prom", + "Prom", + "promql", + "Promql", + "promQL", + "PromQL", + ], + mimetypes: [], + loader: () => + Promise.resolve({ + language, + languageConfiguration, + completionItemProvider: { + provideCompletionItems: () => { + // To simplify, we made the choice to never create automatically the parenthesis behind keywords + // It is because in PromQL, some keywords need parenthesis behind, some don't, some can have but it's optional. + const suggestions = keywords.map((value) => { + return { + label: value, + kind: monaco.languages.CompletionItemKind.Keyword, + insertText: value, + insertTextRules: + monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + }; + }); + + return { suggestions }; + }, + }, + }), +}); + +export const registerPromQlLangauge = (monaco: any) => { + const promLanguageDefinition = loadLanguage(monaco); + const languageId = promLanguageDefinition.id; + monaco.languages.register(promLanguageDefinition); + monaco.languages.onLanguage(languageId, () => { + promLanguageDefinition.loader().then((mod) => { + monaco.languages.setMonarchTokensProvider(languageId, mod.language); + monaco.languages.setLanguageConfiguration( + languageId, + mod.languageConfiguration, + ); + monaco.languages.registerCompletionItemProvider( + languageId, + mod.completionItemProvider, + ); + }); + }); +}; diff --git a/ui-next/src/utils/monitoring.ts b/ui-next/src/utils/monitoring.ts new file mode 100644 index 0000000000..0552c7688c --- /dev/null +++ b/ui-next/src/utils/monitoring.ts @@ -0,0 +1,22 @@ +import { isLogRocketEnabled, reportErrorToLogRocket } from "./logrocket"; + +export const useErrorMonitoring = () => { + return { + notifyError: (error: Error | string, metadata?: { [key: string]: any }) => { + if (isLogRocketEnabled()) { + reportErrorToLogRocket(error, { + tags: { + type: "metadata", + }, + extra: { + ...metadata, + }, + }); + } else { + console.error("=== ERROR ==="); + console.error(error); + console.error("============="); + } + }, + }; +}; diff --git a/ui-next/src/utils/object.ts b/ui-next/src/utils/object.ts new file mode 100644 index 0000000000..ee0e838d52 --- /dev/null +++ b/ui-next/src/utils/object.ts @@ -0,0 +1,51 @@ +import _isPlainObject from "lodash/isPlainObject"; +import _isArray from "lodash/isArray"; + +export const replaceValues = ( + obj: Record, + value: string | number, + newValue: string | number, +) => { + const arrayReplacer = (iv: unknown): unknown => { + if (typeof iv === "string" || typeof iv === "number") { + return iv === value ? newValue : iv; + } else if (_isPlainObject(iv)) { + return replaceValues( + iv as Record, + value, + newValue, + ); + } else if (_isArray(iv)) { + return iv.map(arrayReplacer); + } + return iv; + }; + + return Object.fromEntries( + Object.entries(obj).map(([key, val]): [string | number, unknown] => { + if (_isPlainObject(val)) { + return [ + key, + replaceValues( + val as Record, + value, + newValue, + ), + ]; + } else if (_isArray(val)) { + return [key, val.map(arrayReplacer)]; + } else if (val === value) { + return [key, newValue]; + } + return [key, val]; + }), + ); +}; +export const flipObject = (obj: Record) => + Object.fromEntries(Object.entries(obj).map((a) => a.reverse())); + +export const isObjectOrArray = (value: any): boolean => + (typeof value === "object" && value !== null) || Array.isArray(value); + +export const isObjectOnlyNotArray = (value: any): boolean => + typeof value === "object" && value !== null && !Array.isArray(value); diff --git a/ui-next/src/utils/pipe.ts b/ui-next/src/utils/pipe.ts new file mode 100644 index 0000000000..571435907f --- /dev/null +++ b/ui-next/src/utils/pipe.ts @@ -0,0 +1,30 @@ +interface Pipe { + (value: A): A; + (value: A, fn1: (input: A) => B): B; + (value: A, fn1: (input: A) => B, fn2: (input: B) => C): C; + ( + value: A, + fn1: (input: A) => B, + fn2: (input: B) => C, + fn3: (input: C) => D, + ): D; + ( + value: A, + fn1: (input: A) => B, + fn2: (input: B) => C, + fn3: (input: C) => D, + fn4: (input: D) => E, + ): E; + ( + value: A, + fn1: (input: A) => B, + fn2: (input: B) => C, + fn3: (input: C) => D, + fn4: (input: D) => E, + fn5: (input: E) => F, + ): F; +} + +export const pipe: Pipe = (value: any, ...fns: any[]): unknown => { + return fns.reduce((acc, fn) => fn(acc), value); +}; diff --git a/ui-next/src/utils/query.ts b/ui-next/src/utils/query.ts new file mode 100644 index 0000000000..b9a36e82f9 --- /dev/null +++ b/ui-next/src/utils/query.ts @@ -0,0 +1,755 @@ +import _get from "lodash/get"; +import _isEmpty from "lodash/isEmpty"; +import _sortBy from "lodash/sortBy"; +import { fetchWithContext, useFetchContext } from "plugins/fetch"; +import { pluginRegistry } from "plugins/registry"; +import qs from "qs"; +import { useMemo } from "react"; +import { + useInfiniteQuery, + useMutation, + UseMutationOptions, + UseMutationResult, + useQuery, + useQueryClient, + UseQueryOptions, + UseQueryResult, +} from "react-query"; +import { useLocation, useNavigate } from "react-router"; +import { getAccessToken as getAccessTokenStub } from "shared/auth/tokenManagerJotai"; + +// Get access token from plugin registry (enterprise) or fallback to stub (OSS) +function getAccessToken(): string | null { + // Try plugin registry first (enterprise) + const pluginToken = pluginRegistry.getAccessToken(); + if (pluginToken) { + return pluginToken; + } + // Fallback to stub (OSS - always returns null) + return getAccessTokenStub(); +} +import { WorkflowDef } from "types/WorkflowDef"; +import { AuthHeaders, IObject } from "types/common"; +import { + getUniqueWorkflows, + getUniqueWorkflowsWithVersions, +} from "utils/workflow"; +import { + TASK_EXECUTIONS_SEARCH_URL, + WORKFLOW_METADATA_SHORT_URL, +} from "./constants/api"; +import { HttpStatusCode } from "./constants/httpStatusCode"; +import { ERROR_URL } from "./constants/route"; +import { featureFlags, FEATURES } from "./flags"; +import { logger } from "./logger"; + +// Type definitions +export interface SearchObj { + rowsPerPage: number; + page: number; + sort?: string; + freeText?: string; + query?: string; + queryId?: string; +} + +export interface TaskSearchObj extends Omit { + searchReady?: boolean; +} + +export interface SearchResult { + results: T[]; + totalHits: number; +} + +export interface PollData { + workerId?: string; + domain?: string; + lastPollTime?: number; +} + +export interface TaskQueueInfo { + size?: number; + pollData?: PollData[]; +} + +export interface QueueInfo { + name: string; + size: number; +} + +// Keep MutateParams flexible to allow any additional properties +export interface MutateParams { + path?: string; + method?: string; + body?: any; + [key: string]: any; +} + +// FetchError is any Response-like object with status - kept permissive for backward compatibility +type FetchError = any; + +// Constants +export const STALE_TIME_DROPDOWN = 600000; // 10 mins +export const STALE_TIME_WORKFLOW_DEFS = 600000; // 10 mins +export const STALE_TIME_SECRET_NAMES = 60000; // 1 min +export const STALE_TIME_SEARCH = 60000; // 1 min +export const DEFAULT_STALE_TIME = 5000; // 5 Seconds +export const AUTH_HEADER_NAME = "X-Authorization"; + +export function useFetch( + path: string, + reactQueryOptions?: Partial>, + fetchOptions: IObject = {}, + optionalKey?: string, +): UseQueryResult { + const fetchContext = useFetchContext(); + const fetchParams = { headers: useAuthHeaders() }; + const navigate = useNavigate(); + const location = useLocation(); + const query = useQuery( + optionalKey == null + ? [fetchContext.stack, path] + : [fetchContext.stack, path, optionalKey], + () => + fetchWithContext(path, fetchContext, { ...fetchParams, ...fetchOptions }), + { + // In OSS mode (ACCESS_MANAGEMENT disabled), always enabled when fetchContext is ready + enabled: + fetchContext.ready && + (fetchParams.headers[AUTH_HEADER_NAME] !== undefined || + !featureFlags.isEnabled(FEATURES.ACCESS_MANAGEMENT)), + keepPreviousData: true, + retry: (failureCount: number, error: FetchError) => { + // Don't retry on 403 or 401 + if (error?.status === 403 || error?.status === 401) return false; + return failureCount < 3; + }, + ...reactQueryOptions, + }, + ); + + const statusCode = query?.error?.status; + + // Handle 401 errors by navigating to error page + // In OSS mode, 401 errors shouldn't normally occur since there's no authentication + if (query.isError && statusCode === HttpStatusCode.Unauthorized) { + try { + // Skip navigation for apigateway paths + if (path.startsWith("/gateway")) { + return query; + } + + logger.warn("[useFetch] 401 error, navigating to error page"); + + query.error + ?.clone() + ?.json() + ?.then((result: any) => { + const params = [`code=${statusCode}`]; + if (result?.message) params.push(`message=${result.message}`); + if (result?.error) params.push(`error=${result.error}`); + + if (location.pathname !== ERROR_URL) { + navigate(`${ERROR_URL}?${params.join("&")}`); + } + }); + } catch (error) { + logger.error("[useFetch] error: ", error); + } + } + + return query; +} + +export function useAuthHeaders(): AuthHeaders { + const accessToken = getAccessToken(); + if (accessToken) { + return { [AUTH_HEADER_NAME]: accessToken }; + } + + return {}; +} + +export function useWorkflowSearch( + searchObj: SearchObj, + queryOption: Partial> = {}, + queryOptionOverride: Partial> = {}, +): UseQueryResult { + return useSearch( + searchObj, + "/workflow/search?", + queryOption, + queryOptionOverride, + ); +} + +export function useSchedulerSearch( + searchObj: SearchObj, +): UseQueryResult { + return useSearch(searchObj, "/scheduler/search/executions?"); +} + +export function useTaskExecutionsSearch( + searchObj: SearchObj, + queryOptionOverride: Partial> = {}, +): UseQueryResult { + return useSearch( + searchObj, + TASK_EXECUTIONS_SEARCH_URL, + {}, + queryOptionOverride, + ); +} + +export function useSearch( + searchObj: SearchObj, + pathRoot: string, + queryOption: Partial> = {}, + queryOptionsOverride: Partial> = {}, +): UseQueryResult { + const fetchContext = useFetchContext(); + const fetchParams = { headers: useAuthHeaders() }; + + return useQuery( + [fetchContext.stack, pathRoot, searchObj], + () => { + const { rowsPerPage, page, sort, freeText, query } = searchObj; + let params: IObject = { + start: (page - 1) * rowsPerPage, + size: rowsPerPage, + sort: sort, + freeText: freeText, + query: query, + }; + if (searchObj.queryId) { + params = { queryId: searchObj.queryId, ...params }; + } + const path = pathRoot + qs.stringify(params); + return fetchWithContext(path, fetchContext, fetchParams); + // staletime to ensure stable view when paginating back and forth (even if underlying results change) + }, + { + enabled: + typeof queryOption.enabled === "boolean" + ? queryOption.enabled + : fetchContext.ready, + keepPreviousData: true, + staleTime: STALE_TIME_SEARCH, + retry: (failureCount: number, error: FetchError) => { + if (error?.status >= 400 && error.status < 500) { + return false; + } + return failureCount - 2 > 0; + }, + ...queryOptionsOverride, + }, + ); +} + +// @Deprecated +export function useTaskSearch({ + searchReady, + ...searchObj +}: TaskSearchObj & { searchReady?: boolean }) { + const fetchContext = useFetchContext(); + const queryClient = useQueryClient(); + const fetchParams = { headers: useAuthHeaders() }; + + const pathRoot = "/workflow/search-by-tasks?"; + const key = [fetchContext.stack, pathRoot, searchObj]; + + const infiniteQuery = useInfiniteQuery( + key, + ({ pageParam = 0 }) => { + const { rowsPerPage, sort, freeText, query } = searchObj; + + if (!searchReady) { + return Promise.resolve({ results: [] }); + } + + const path = + pathRoot + + qs.stringify({ + start: rowsPerPage * pageParam, + size: rowsPerPage, + sort: sort, + freeText: freeText, + query: query, + }); + return fetchWithContext(path, fetchContext, fetchParams); + }, + { + getNextPageParam: (_lastPage, pages) => pages.length, + }, + ); + + return { + ...infiniteQuery, + refetch: () => { + queryClient.refetchQueries(key); + }, + }; +} + +export function useTaskQueueInfo(taskName: string): { + data: TaskQueueInfo; + isFetching: boolean; +} { + const fetchContext = useFetchContext(); + const fetchParams = { headers: useAuthHeaders() }; + + const pollDataPath = `/tasks/queue/polldata?taskType=${taskName}`; + const sizePath = `/tasks/queue/sizes?taskType=${taskName}`; + + const { data: pollData, isFetching: pollDataFetching } = useQuery< + PollData[], + FetchError + >( + [fetchContext.stack, pollDataPath], + () => fetchWithContext(pollDataPath, fetchContext, fetchParams), + { + enabled: fetchContext.ready && !_isEmpty(taskName), + }, + ); + const { data: size, isFetching: sizeFetching } = useQuery< + Record, + FetchError + >( + [fetchContext.stack, sizePath], + () => fetchWithContext(sizePath, fetchContext, fetchParams), + { + enabled: fetchContext.ready && !_isEmpty(taskName), + }, + ); + + const taskQueueInfo = useMemo( + () => ({ size: _get(size, [taskName]), pollData: pollData }), + [taskName, pollData, size], + ); + + return { + data: taskQueueInfo, + isFetching: pollDataFetching || sizeFetching, + }; +} + +export function useAction( + path: string, + method = "post", + callbacks?: any, + isText?: boolean, +) { + const fetchContext = useFetchContext(); + const authHeaders = useAuthHeaders(); + + return useMutation( + (mutateParams) => + fetchWithContext( + path, + fetchContext, + { + method, + headers: { + "Content-Type": "application/json", + ...authHeaders, + }, + body: _get(mutateParams, "body"), + }, + isText, + ), + callbacks, + ); +} + +export function useActionWithPath( + callbacks?: UseMutationOptions, + isText?: boolean, + throwOnError?: boolean, +): UseMutationResult { + const fetchContext = useFetchContext(); + const authHeaders = useAuthHeaders(); + return useMutation((mutateParams) => { + const actionPath = _get(mutateParams, "path") as string; + const method = _get(mutateParams, "method") as string; + const contentType = isText ? "text/plain" : "application/json"; + return fetchWithContext( + actionPath, + fetchContext, + { + method, + headers: { + "Content-Type": contentType, + ...authHeaders, + }, + body: _get(mutateParams, "body"), + }, + isText, + throwOnError, + ); + }, callbacks); +} + +export function useUsersListing(includeApps = false) { + const { data, ...rest } = useFetch(`/users?apps=${includeApps}`, { + staleTime: DEFAULT_STALE_TIME, + }); + return { + data: useMemo(() => (data ? data : []), [data]), + ...rest, + }; +} + +export function useWorkflowDefs( + optionsOverride: Partial> = {}, +): UseQueryResult { + return useFetch(WORKFLOW_METADATA_SHORT_URL, { + staleTime: DEFAULT_STALE_TIME, + ...optionsOverride, + }); +} + +export function useWorkflowNames( + optionsOverride: Partial> = {}, +): string[] { + const { data } = useWorkflowDefs(optionsOverride); + + // Filter latest versions only + const workflows = useMemo(() => { + if (data) { + return getUniqueWorkflows(data); + } + }, [data]); + + return useMemo( + () => (workflows ? workflows.map((def) => def.name) : []), + [workflows], + ); +} + +export const useSharedQueryContext = (): { + url: string; + cacheQueryKey: (string | undefined)[]; + fetchContext: ReturnType; +} => { + const fetchContext = useFetchContext(); + const url = WORKFLOW_METADATA_SHORT_URL; + const cacheQueryKey = [fetchContext.stack, url]; + return { url, cacheQueryKey: cacheQueryKey, fetchContext }; +}; + +export const usePrefetchWorkflows = (): void => { + const headers = useAuthHeaders(); + const { url, fetchContext, cacheQueryKey } = useSharedQueryContext(); + const queryClient = useQueryClient(); + + // In OSS mode, always prefetch (no authentication check needed) + const fetchParams = { headers }; + queryClient.prefetchQuery({ + queryKey: cacheQueryKey, + queryFn: () => fetchWithContext(url, fetchContext, fetchParams), + staleTime: STALE_TIME_WORKFLOW_DEFS, + }); +}; + +// Version numbers do not necessarily start, or run contiguously from 1. Could arbitrary integers e.g. 52335678. +// By convention they should be monotonic (ever increasing) wrt time. +// @Deprecated use useWorkflowNamesAndVersionsQuery instead +export function useWorkflowNamesAndVersions(): Map { + const { url } = useSharedQueryContext(); + const { data } = useFetch(url, { + staleTime: STALE_TIME_WORKFLOW_DEFS, + }); + + return useMemo(() => getUniqueWorkflowsWithVersions(data), [data]); +} + +export function useWorkflowDefsByVersions({ + queryParams = {}, +}: { queryParams?: IObject | string } = {}) { + const queryString = + typeof queryParams === "object" && !Array.isArray(queryParams) + ? qs.stringify(queryParams, { addQueryPrefix: true }) + : queryParams; + + const { data } = useFetch(`/metadata/workflow${queryString}`, { + staleTime: STALE_TIME_WORKFLOW_DEFS, + }); + + return useMemo(() => { + const retval = new Map(); + const lookups = new Map(); + const values = new Map(); + if (data) { + for (const def of data) { + let lArr: any[]; + let vMap: Map; + if (!lookups.has(def.name)) { + lArr = []; + vMap = new Map(); + lookups.set(def.name, lArr); + values.set(def.name, vMap); + } else { + lArr = lookups.get(def.name); + vMap = values.get(def.name); + } + lArr.push(def.version.toString()); // Someone will eventually come back to this. + vMap.set(def.version.toString(), def); + } + + // Sort arrays in place + lookups.forEach((val, key) => { + // Sort versions + lookups.set( + key, + _sortBy(val, (val) => Number(val)), + ); + }); + } + + retval.set("lookups", lookups); + retval.set("values", values); + + return retval; + }, [data]); +} + +export function useTaskNames(access?: string) { + const queryParams = access ? `?access=${access}` : ""; + const { data } = useFetch(`/metadata/taskdefs${queryParams}`, { + staleTime: STALE_TIME_DROPDOWN, + }); + return useMemo( + () => + data ? Array.from(new Set(data.map((def: any) => def.name))).sort() : [], + [data], + ); +} + +export function useGroupsListing() { + const { data, ...rest } = useFetch("/groups", { + staleTime: DEFAULT_STALE_TIME, + }); + return { + data: useMemo(() => (data ? data : []), [data]), + ...rest, + }; +} + +export function useRolesListing() { + const { data, ...rest } = useFetch("/roles", { + staleTime: DEFAULT_STALE_TIME, + }); + return { + data: useMemo(() => (data ? data : []), [data]), + ...rest, + }; +} + +export function useCustomRolesListing() { + const { data, ...rest } = useFetch("/roles/custom", { + staleTime: DEFAULT_STALE_TIME, + }); + return { + data: useMemo(() => (data ? data : []), [data]), + ...rest, + }; +} + +export function useSystemRoles() { + const { data, ...rest } = useFetch("/roles/system", { + staleTime: DEFAULT_STALE_TIME, + }); + return { + data: useMemo(() => (data ? data : {}), [data]), + ...rest, + }; +} + +export function useAvailablePermissions() { + const { data, ...rest } = useFetch("/roles/permissions", { + staleTime: DEFAULT_STALE_TIME, + }); + return { + data: useMemo(() => (data?.permissions ? data.permissions : []), [data]), + ...rest, + }; +} + +export function useSingleRole(roleId?: string) { + const { data, ...rest } = useFetch(`/roles/${roleId}`, { + staleTime: DEFAULT_STALE_TIME, + enabled: !!roleId, + }); + return { + data: useMemo(() => data, [data]), + ...rest, + }; +} + +export function useGroupUsers(id: string) { + const { data, ...rest } = useFetch(`/groups/${id}/users`, { + staleTime: DEFAULT_STALE_TIME, + }); + return { + data: useMemo(() => (data ? data : []), [data]), + ...rest, + }; +} + +export function useUserById(id: string) { + const { data, ...rest } = useFetch(`/users/${id}`, { + staleTime: DEFAULT_STALE_TIME, + }); + return { + data: data || {}, + ...rest, + }; +} + +export function useUserPermissions(id: string) { + const { data, ...rest } = useFetch(`/users/${id}/permissions`, { + staleTime: DEFAULT_STALE_TIME, + }); + return { + data: useMemo(() => (data ? data : {}), [data]), + ...rest, + }; +} + +//TODO consider adding an API operation to get the Workflow definition names from the backend +export function useWorkflowDefNames(access?: string) { + const queryParams = access ? `?access=${access}` : ""; + const { data, ...rest } = useFetch( + `/metadata/workflow${queryParams}&short=true`, + { + staleTime: STALE_TIME_WORKFLOW_DEFS, + }, + ); + + const extractNames = (defs: WorkflowDef[]): string[] => { + const names = defs.map((def) => def.name); + return [...new Set(names)].sort(); + }; + + return { + data: data ? extractNames(data) : [], + ...rest, + }; +} + +export function useSecretNames(): string[] { + const { data } = useFetch(`/secrets`, { + staleTime: STALE_TIME_SECRET_NAMES, + }); + return data ? data : []; +} + +export function useAppListing() { + const { data, ...rest } = useFetch("/applications", { + staleTime: DEFAULT_STALE_TIME, + }); + return { + data: data || [], + ...rest, + }; +} + +export function useApplicationById(id: string) { + const { data, ...rest } = useFetch(`/applications/${id}`, { + staleTime: DEFAULT_STALE_TIME, + }); + return { + data: data || {}, + ...rest, + }; +} + +export function useAccessKeysListing(applicationId: string) { + const { data, ...rest } = useFetch( + `/applications/${applicationId}/accessKeys`, + { + staleTime: DEFAULT_STALE_TIME, + }, + ); + return { + data: data || [], + ...rest, + }; +} + +export const useCurrentUserInfo = (function () { + if (featureFlags.isEnabled(FEATURES.ACCESS_MANAGEMENT)) { + return () => { + return useFetch(`/token/userInfo`, { + staleTime: DEFAULT_STALE_TIME, + retry: false, // Don't retry when fetching token + }); + }; + } else { + // if access management is not enabled then just return data: {} + return () => { + return { data: {}, isFetching: false, isError: false } as const; + }; + } +})(); + +export const useAPIReleaseVersion = ({ + keys = [], + option, +}: { + keys?: string[]; + option?: any; +} = {}) => { + return useQuery( + keys, + () => fetchWithContext("/version", null as any, null as any, true), + { + staleTime: Infinity, + retry: (failureCount: number, error: any) => { + if (error?.status >= 400 && error.status < 500) { + return false; + } + return failureCount - 2 > 0; + }, + onSuccess: (data: string) => { + localStorage.setItem("version", data); + }, + ...option, + }, + ); +}; + +export function useTags( + reactQueryOptions?: Partial>, +) { + const { data, ...rest } = useFetch("/metadata/tags", { + staleTime: DEFAULT_STALE_TIME, + ...reactQueryOptions, + }); + + return { + data, + ...rest, + }; +} + +export function useQueueDepth() { + const { data, ...rest } = useFetch("/tasks/queue/all", { + staleTime: DEFAULT_STALE_TIME, + }); + const myData: QueueInfo[] = []; + for (const i in data) { + const queueInfo = { name: i, size: data[i] }; + myData.push(queueInfo); + } + + // fitering internal queues + const filteredData = myData.filter((el) => !el.name.startsWith("_")); + + const sortedData = filteredData.sort((a, b) => b.size - a.size); + return { + data: sortedData, + ...rest, + }; +} diff --git a/ui-next/src/utils/reactHookForm.ts b/ui-next/src/utils/reactHookForm.ts new file mode 100644 index 0000000000..ef76fc06a3 --- /dev/null +++ b/ui-next/src/utils/reactHookForm.ts @@ -0,0 +1,118 @@ +import _isArray from "lodash/isArray"; +import _isNull from "lodash/isNull"; +import _isObject from "lodash/isObject"; +import _mapValues from "lodash/mapValues"; +import _omitBy from "lodash/omitBy"; +import { FieldErrors, FieldValues } from "react-hook-form"; + +export const getEditorToFormValue = ( + formValues: FieldValues, + editorValue: FieldValues, + hiddenKeys: string[] = [], +): any => { + const result: FieldValues = {}; + + // Preserve the order of keys from obj2 + const editorValueKeys = Object.keys(editorValue); + + // Merge keys from both objects, but prioritize editorValue order + const mergedKeys = Array.from( + new Set([...editorValueKeys, ...Object.keys(formValues)]), + ); + + // Iterate over merged keys + mergedKeys.forEach((key) => { + // Check if key is not in hiddenKeys array + if (!hiddenKeys.includes(key)) { + // If key exists in editorValue + if (Object.prototype.hasOwnProperty.call(editorValue, key)) { + // If the value is null or undefined, handle accordingly + if (editorValue[key] === null || editorValue[key] === undefined) { + result[key] = editorValue[key]; + } else if ( + typeof editorValue[key] === "object" && + !Array.isArray(editorValue[key]) + ) { + // If the value is an object, recursively update + result[key] = getEditorToFormValue( + formValues[key] || {}, + editorValue[key], + hiddenKeys, + ); + } else if (Array.isArray(editorValue[key])) { + // If the value is an array, handle each element + result[key] = editorValue[key].map((item: any, index: number) => { + // If the element is an object, recursively update + if (typeof item === "object" && !Array.isArray(item)) { + return getEditorToFormValue( + formValues[key]?.[index] || {}, + item, + hiddenKeys, + ); + } + // Otherwise, return the element + return item; + }); + } else { + // Otherwise, assign the value directly + result[key] = editorValue[key]; + } + } else { + // If key does not exist in editorValue, set null + result[key] = null; + } + } else { + // Otherwise, keep the value from formValues + result[key] = formValues[key]; + } + }); + + return result; +}; + +export const getFormToEditorValue = ( + formValues: FieldValues, + hiddenKeys: string[] = [], +) => { + return JSON.stringify( + removeNullAndHiddenKeys(formValues, hiddenKeys), + null, + 2, + ); +}; + +export const getReactHookFormError = ( + errors: FieldErrors, +): string | null => { + for (const key in errors) { + const error = errors[key]; + if (typeof error === "object" && error !== null) { + const errorMessage = getReactHookFormError(error as FieldErrors); + if (errorMessage) { + return errorMessage; + } + } else if (typeof error === "string") { + return error; + } + } + return null; +}; + +export const removeNullAndHiddenKeys = ( + value: any, + hiddenKeys: string[] = [], +): object => { + if (_isArray(value)) { + return value + .map((val) => removeNullAndHiddenKeys(val, hiddenKeys)) + .filter(Boolean); + } + if (_isObject(value)) { + return _omitBy( + _mapValues(value, (value) => removeNullAndHiddenKeys(value, hiddenKeys)), + (value, key) => _isNull(value) || hiddenKeys?.includes(key), + ); + } + + return value; +}; diff --git a/ui-next/src/utils/regex.ts b/ui-next/src/utils/regex.ts new file mode 100644 index 0000000000..e41a65abd9 --- /dev/null +++ b/ui-next/src/utils/regex.ts @@ -0,0 +1 @@ +export const OBJECT_PROPERTY_NAME_REGEX = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/; diff --git a/ui-next/src/utils/releaseVersion.ts b/ui-next/src/utils/releaseVersion.ts new file mode 100644 index 0000000000..cc428f22fa --- /dev/null +++ b/ui-next/src/utils/releaseVersion.ts @@ -0,0 +1,4 @@ +export const releaseVersion = + process.env?.VITE_CONDUCTOR_UI_VERSION == null + ? "latest" + : process.env.VITE_CONDUCTOR_UI_VERSION; diff --git a/ui-next/src/utils/remoteServices.ts b/ui-next/src/utils/remoteServices.ts new file mode 100644 index 0000000000..15f5248e34 --- /dev/null +++ b/ui-next/src/utils/remoteServices.ts @@ -0,0 +1,57 @@ +/** + * Utility functions for remote service operations. + * Extracted from pages/remoteServices so OSS code can use them without + * importing from an enterprise page. + */ + +export function splitHostAndPort(url = "") { + // Split by ":" to separate host and port + const [host, port] = url.split(/:(?=\d+$)/); + + // If there's no port, return null for port + return { host, port: Number(port) || null }; +} + +export function replaceDynamicParams( + url: string, + params: Record>, +): { url: string; headers?: Record } { + // Replace path parameters in the URL + const pathReplaced = url.replace(/\{(\w+)\}/g, (_, key: string): string => { + const param = params[key]; + return param && param?.type === "path" && param?.value != null + ? (param.value as string) + : `{${key}}`; // fallback to original if missing + }); + + // Collect query parameters + const queryParams = Object.values(params) + ?.filter( + (param) => + param.type === "query" && + param.value != null && + param.value !== undefined && + param.value !== "", + ) + ?.map((param) => `${param?.name}=${param.value}`); + + const queryString = queryParams.length ? `?${queryParams.join("&")}` : ""; + + // Collect headers if available + const headersEntries = Object.values(params) + ?.filter( + (param) => + param.type === "header" && + param.value != null && + param.value !== undefined && + param.value !== "", + ) + ?.map((param) => [param.name as string, String(param.value)]); + const headers = + headersEntries.length > 0 ? Object.fromEntries(headersEntries) : undefined; + + return { + url: pathReplaced + queryString, + ...(headers ? { headers } : {}), + }; +} diff --git a/ui-next/src/utils/roles.ts b/ui-next/src/utils/roles.ts new file mode 100644 index 0000000000..e2b7d8949f --- /dev/null +++ b/ui-next/src/utils/roles.ts @@ -0,0 +1,44 @@ +import { + roleAdmin, + roleMetaManager, + roleReadOnly, + roleUser, + roleWfManager, +} from "theme/tokens/colors"; +import { AccessRole } from "types/User"; +import { Role } from "utils/accessControl"; + +export const roleLabel: { [key: string]: string } = { + [Role.ADMIN]: "Admin", + [Role.USER]: "User", + [Role.METADATA_MANAGER]: "Metadata manager", + [Role.WORKFLOW_MANAGER]: "Workflow manager", + [Role.USER_READ_ONLY]: "Read only user", +}; + +export const userRoleColorGenerator = (role: string) => { + let tagColor; + if (role === Role.ADMIN) { + tagColor = roleAdmin; + } else if (role === Role.USER) { + tagColor = roleUser; + } else if (role === Role.WORKFLOW_MANAGER) { + tagColor = roleWfManager; + } else if (role === Role.METADATA_MANAGER) { + tagColor = roleMetaManager; + } else { + tagColor = roleReadOnly; + } + return { backgroundColor: tagColor }; +}; + +export const sortRoles = (roles?: AccessRole[]) => + (roles ?? []).sort((a: { name: string }, b: { name: string }) => { + if (a.name < b.name) { + return -1; + } + if (a.name > b.name) { + return 1; + } + return 0; + }); diff --git a/ui-next/src/utils/strings.ts b/ui-next/src/utils/strings.ts new file mode 100644 index 0000000000..cc8c8f2f36 --- /dev/null +++ b/ui-next/src/utils/strings.ts @@ -0,0 +1,50 @@ +import _lowerCase from "lodash/lowerCase"; +import _upperFirst from "lodash/upperFirst"; + +import { findNextMissingSequentialNumber } from "utils/utils"; + +export const randomChars = (n = 7): string => + (Math.random() + 1).toString(36).substring(n); + +export const getSequentiallySuffix = ({ + name, + refNames, +}: { + name: string; + refNames: string[]; +}) => { + // Finding a suffix number array + // Because the task name can be modified, so the suffix number maybe not sequential + // ex: The original: [1,2,3] + // after modifying it can be: [15,2,3] + const taskNumbers = refNames.reduce((acc, taskReferenceName) => { + if (taskReferenceName) { + if (taskReferenceName.startsWith(`${name}`)) { + const lastNumber = Number(taskReferenceName.replace(`${name}_`, "")); + + if (lastNumber > 0) { + return [...acc, lastNumber]; + } + + return [...acc, 0]; + } + } + + return acc; + }, [] as number[]); + + let missingNum = findNextMissingSequentialNumber(taskNumbers); + + if (missingNum === null) { + missingNum = taskNumbers[taskNumbers.length - 1] + 1; + } + + const suffixString = missingNum ? `_${missingNum}` : ""; + + return { + name: `${name.replace("_ref", "")}${suffixString}`, + taskReferenceName: `${name}${suffixString}`, + }; +}; + +export const toUpperFirst = (str: string) => _upperFirst(_lowerCase(str)); diff --git a/ui-next/src/utils/task.ts b/ui-next/src/utils/task.ts new file mode 100644 index 0000000000..511a49aef2 --- /dev/null +++ b/ui-next/src/utils/task.ts @@ -0,0 +1,70 @@ +import { + CommonTaskDef, + ForkJoinDynamicDef, + JDBCTaskDef, + LLMTaskTypes, + SetVariableTaskDef, +} from "types/TaskType"; +import { TASK_STATUS } from "./constants/task"; +import { flipObject } from "./object"; +import { TaskType } from "types/common"; +import { HumanTaskDef } from "types/HumanTaskTypes"; + +const taskOrder = [ + TASK_STATUS.SCHEDULED, + TASK_STATUS.IN_PROGRESS, + TASK_STATUS.SKIPPED, + TASK_STATUS.COMPLETED, + TASK_STATUS.COMPLETED_WITH_ERRORS, + TASK_STATUS.TIMED_OUT, + TASK_STATUS.FAILED, + TASK_STATUS.FAILED_WITH_TERMINAL_ERROR, +]; + +const arrayAsObject: Record = Object.assign({}, taskOrder); + +const taskOrderObject = flipObject(arrayAsObject); + +export const taskStatusCompareFn = (a: string, b: string) => { + if (taskOrderObject[a] < taskOrderObject[b]) { + return -1; + } + if (taskOrderObject[a] > taskOrderObject[b]) { + return 1; + } + return 0; +}; + +const LLMTaskTypesTypes = [ + TaskType.LLM_TEXT_COMPLETE, + TaskType.LLM_GENERATE_EMBEDDINGS, + TaskType.LLM_GET_EMBEDDINGS, + TaskType.LLM_STORE_EMBEDDINGS, + TaskType.LLM_INDEX_DOCUMENT, + TaskType.LLM_SEARCH_INDEX, + TaskType.GET_DOCUMENT, + TaskType.LLM_INDEX_TEXT, + TaskType.LLM_CHAT_COMPLETE, +]; + +export const TaskTypesStrings = Object.values(TaskType); +// Task Predicates +export const isTask = (value: any): value is CommonTaskDef => + "type" in value && TaskTypesStrings.includes(value.type); + +export const isLLMTask = (task: CommonTaskDef): task is LLMTaskTypes => + LLMTaskTypesTypes.includes(task.type); + +export const isHumanTask = (task: CommonTaskDef): task is HumanTaskDef => + task.type === "HUMAN"; + +export const isSetVariable = ( + task: CommonTaskDef, +): task is SetVariableTaskDef => task.type === "SET_VARIABLE"; + +export const isDynamicForkTask = ( + task: CommonTaskDef, +): task is ForkJoinDynamicDef => task.type === "FORK_JOIN_DYNAMIC"; + +export const isJDBCTask = (task: CommonTaskDef): task is JDBCTaskDef => + task.type === "JDBC"; diff --git a/ui-next/src/utils/themeVariables.ts b/ui-next/src/utils/themeVariables.ts new file mode 100644 index 0000000000..4c15630ae1 --- /dev/null +++ b/ui-next/src/utils/themeVariables.ts @@ -0,0 +1,7 @@ +import { orkesTheme } from "theme/tokens/orkes-theme"; + +export const getThemeAsCSSVariables = (): string[] => { + return Array.from(Object.keys(orkesTheme)).map((name) => { + return `--${name}: ${(orkesTheme as any)[name]};`; + }); +}; diff --git a/ui-next/src/utils/toMaybeQueryString.ts b/ui-next/src/utils/toMaybeQueryString.ts new file mode 100644 index 0000000000..22dbcb2ba8 --- /dev/null +++ b/ui-next/src/utils/toMaybeQueryString.ts @@ -0,0 +1,37 @@ +import _isEmpty from "lodash/isEmpty"; +import _pickBy from "lodash/pickBy"; +import _isNil from "lodash/isNil"; + +export type UrlOptions = + | string + | string[][] + | Record + | URLSearchParams + | undefined; + +export const toMaybeQueryString = ( + qOptions: UrlOptions, + prefixChar: "?" | "&" = "?", +): string => { + const cleanedObject = _pickBy( + qOptions as object, + (a) => !_isNil(a), + ) as UrlOptions; + return _isEmpty(qOptions) + ? "" + : `${prefixChar}${ + new URLSearchParams(cleanedObject).toString() // filter out undefined values + }`; +}; + +export const urlWithQueryParameters = ( + url: string, + qOptions: UrlOptions, +): string => { + try { + const hasParams = [...new URL(url).searchParams]?.length; + return url + toMaybeQueryString(qOptions, hasParams ? "&" : "?"); + } catch { + return url + toMaybeQueryString(qOptions, "?"); + } +}; diff --git a/ui-next/src/utils/tracker.tsx b/ui-next/src/utils/tracker.tsx new file mode 100644 index 0000000000..dc6ffe90de --- /dev/null +++ b/ui-next/src/utils/tracker.tsx @@ -0,0 +1,18 @@ +import { Helmet } from "react-helmet"; +import { featureFlags, FEATURES } from "./flags"; + +const isPlayground = featureFlags.isEnabled(FEATURES.PLAYGROUND); +const isUserManagement = featureFlags.isEnabled(FEATURES.ACCESS_MANAGEMENT); +const logRocketKey = featureFlags.getValue(FEATURES.HEAP_APP_ID); +const isHeapEnabled = () => isPlayground && isUserManagement && logRocketKey; + +export const MaybeHeapHelmet = () => { + return isHeapEnabled() ? ( + + + + ) : null; +}; diff --git a/ui-next/src/utils/useGetGroups.ts b/ui-next/src/utils/useGetGroups.ts new file mode 100644 index 0000000000..bce1afdb5d --- /dev/null +++ b/ui-next/src/utils/useGetGroups.ts @@ -0,0 +1,31 @@ +import { fetchWithContext, useFetchContext } from "plugins/fetch"; +import { useQuery } from "react-query"; +import { AccessGroup } from "types"; +import { STALE_TIME_SEARCH, useAuthHeaders } from "utils/query"; + +const GROUPS_PATH = "/groups"; + +export const useGetGroups = () => { + const fetchContext = useFetchContext(); + const fetchParams = { headers: useAuthHeaders() }; + + return useQuery( + [fetchContext.stack, GROUPS_PATH, {}], + () => { + const path = GROUPS_PATH; + return fetchWithContext(path, fetchContext, fetchParams); + // staletime to ensure stable view when paginating back and forth (even if underlying results change) + }, + { + enabled: fetchContext.ready, + keepPreviousData: true, + staleTime: STALE_TIME_SEARCH, + retry: (failureCount: number, error: any) => { + if (error?.status >= 400 && error.status < 500) { + return false; + } + return failureCount > 3; + }, + }, + ); +}; diff --git a/ui-next/src/utils/useGetUsers.ts b/ui-next/src/utils/useGetUsers.ts new file mode 100644 index 0000000000..dcecf01032 --- /dev/null +++ b/ui-next/src/utils/useGetUsers.ts @@ -0,0 +1,31 @@ +import { fetchWithContext, useFetchContext } from "plugins/fetch"; +import { useAuthHeaders, STALE_TIME_SEARCH } from "utils/query"; +import { useQuery } from "react-query"; +import { User } from "types"; + +const USERS_PATH = "/users"; + +export const useGetUsers = () => { + const fetchContext = useFetchContext(); + const fetchParams = { headers: useAuthHeaders() }; + + return useQuery( + [fetchContext.stack, USERS_PATH, {}], + () => { + const path = USERS_PATH; + return fetchWithContext(path, fetchContext, fetchParams); + // staletime to ensure stable view when paginating back and forth (even if underlying results change) + }, + { + enabled: fetchContext.ready, + keepPreviousData: true, + staleTime: STALE_TIME_SEARCH, + retry: (failureCount: number, error: any) => { + if (error?.status >= 400 && error.status < 500) { + return false; + } + return failureCount > 3; + }, + }, + ); +}; diff --git a/ui-next/src/utils/useIntegrationProviders.ts b/ui-next/src/utils/useIntegrationProviders.ts new file mode 100644 index 0000000000..452bccbbcc --- /dev/null +++ b/ui-next/src/utils/useIntegrationProviders.ts @@ -0,0 +1,20 @@ +import { toMaybeQueryString } from "./toMaybeQueryString"; +import { INTEGRATIONS_API_URL } from "./constants/api"; +import { useFetch, STALE_TIME_DROPDOWN } from "./query"; +import { IntegrationCategory } from "types/Integrations"; + +export function useIntegrationProviders({ + category, + activeOnly, +}: { + category: IntegrationCategory; + activeOnly: boolean; +}) { + const maybeQueryString = toMaybeQueryString({ category, activeOnly }); + const url = `${INTEGRATIONS_API_URL.PROVIDER}${maybeQueryString}`; + + const result = useFetch(url, { + staleTime: STALE_TIME_DROPDOWN, + }); + return result; +} diff --git a/ui-next/src/utils/useInterval.ts b/ui-next/src/utils/useInterval.ts new file mode 100644 index 0000000000..4ddd4bb871 --- /dev/null +++ b/ui-next/src/utils/useInterval.ts @@ -0,0 +1,30 @@ +import { useEffect, useRef, useLayoutEffect } from "react"; + +// See: https://usehooks-ts.com/react-hook/use-isomorphic-layout-effect + +const useIsomorphicLayoutEffect = + typeof window !== "undefined" ? useLayoutEffect : useEffect; + +function useInterval(callback: () => void, delay: number | null) { + const savedCallback = useRef(callback); + + // Remember the latest callback if it changes. + useIsomorphicLayoutEffect(() => { + savedCallback.current = callback; + }, [callback]); + + // Set up the interval. + useEffect(() => { + // Don't schedule if no delay is specified. + // Note: 0 is a valid value for delay. + if (!delay && delay !== 0) { + return; + } + + const id = setInterval(() => savedCallback.current(), delay); + + return () => clearInterval(id); + }, [delay]); +} + +export default useInterval; diff --git a/ui-next/src/utils/useLazyWorkflowNameAutoComplete.ts b/ui-next/src/utils/useLazyWorkflowNameAutoComplete.ts new file mode 100644 index 0000000000..4b555e11b3 --- /dev/null +++ b/ui-next/src/utils/useLazyWorkflowNameAutoComplete.ts @@ -0,0 +1,15 @@ +import { useMemo, useState } from "react"; +import { useWorkflowNames } from "./query"; + +export const useLazyWorkflowNameAutoComplete = ( + nameFilter = (_x: string) => true, +): [() => void, string[]] => { + const [fetch, setEnableFetch] = useState(false); + const workflowNames = useWorkflowNames({ enabled: fetch }); + const names = useMemo((): string[] => { + return workflowNames + .filter(nameFilter) + .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); + }, [workflowNames, nameFilter]); + return [() => setEnableFetch(true), names]; +}; diff --git a/ui-next/src/utils/utils.ts b/ui-next/src/utils/utils.ts new file mode 100644 index 0000000000..5620cf939a --- /dev/null +++ b/ui-next/src/utils/utils.ts @@ -0,0 +1,385 @@ +import { JsonSchema } from "@jsonforms/core"; +import _capitalize from "lodash/fp/capitalize"; +import _defaultTo from "lodash/fp/defaultTo"; +import isEmpty from "lodash/isEmpty"; +import isNil from "lodash/isNil"; +import _mapValues from "lodash/mapValues"; +import _pickBy from "lodash/pickBy"; +import { useCallback, useState } from "react"; +import { TagDto } from "types/Tag"; +import { + ErrorObj, + FIELD_TYPE_OBJECT, + TaskDef, + TaskType, + TryFn, +} from "types/common"; +import { inferType } from "./helpers"; +import { logger } from "./logger"; + +/** + * When there are validation errors the backend will respond with something like: + * + * (2) + * { + * "message" : "..." + * "validationErrors": [ + * { + * "path": "ownerEmail", + * "message": "ownerEmail cannot be empty" + * } + * ] + * ... + * } + * + * This function returns an object with the errors as properties e.g.: + * { "ownerEmail": "ownerEmail cannot be empty" } + * and the message if present. + * + * NOTES: path may take this form if it's a list registerTaskDef.taskDefinitions[0].ownerEmail. + * + * "message" may be a generic error message or a comma separated list of all messages. + * + * @param response Fetch response object + * @returns if "errors" exists in the response an object which properties are the errors. + */ +export const GENERIC_ERROR = "Error performing action. error number:"; + +const defaultToEmpty = _defaultTo(""); + +export const defaultGenericErrorHandler = (response: Response) => ({ + message: `${GENERIC_ERROR} ${defaultToEmpty( + String(response?.status), + )} ${defaultToEmpty(response.statusText)}`, +}); + +export const getErrors = async ( + response: Response, + genericErrorHandler = defaultGenericErrorHandler, +) => { + const contentType = response.headers?.get("content-type"); + if (isNil(contentType) || contentType.indexOf("application/json") === -1) { + console.error("Body is not even json. Check Response! ", response); + return genericErrorHandler(response); + } + + const clonedResponse = response.clone(); + const body = await clonedResponse.json(); + + if (isEmpty(body?.validationErrors) && isEmpty(body?.message)) { + console.error( + Object.assign(Error("No error messages in response"), { body }), + ); + return genericErrorHandler(clonedResponse); + } + + return Object.assign( + { message: body.message }, + ...(isEmpty(body.validationErrors) + ? [] + : body.validationErrors.map( + (error: { path: string; message: string }) => ({ + [error.path]: error.message, + }), + )), + ); +}; + +export const getErrorMessage = async (response: Response): Promise => { + const parsedError = await getErrors(response); + + if (parsedError?.message) { + return parsedError?.message; + } + + return ""; +}; + +export const tryFunc = async ({ + fn, + customError, + showCustomError = true, +}: { + fn: TryFn; + customError?: E; + showCustomError?: boolean; +}) => { + try { + return await fn(); + } catch (error: any) { + logger.error("[tryFunc] error:", error); + const details = await getErrors(error); + + return Promise.reject({ + ...customError, + originalError: details, + message: + !showCustomError && details?.message != null + ? details?.message + : customError?.message, + }); + } +}; + +export const capitalizeFirstLetter = _capitalize; + +export const getTitleSuffix = (type?: string, id?: string) => { + if (type) { + return ` - ${capitalizeFirstLetter(type)}` + (id ? ` - ${id}` : ""); + } + return ""; +}; + +export const tryToJson = (str?: string | null): T | undefined => { + if (str == null) { + return undefined; + } + try { + return JSON.parse(str) as T; + } catch (error) { + logger.error(`Error parsing JSON: ${error}`); + return undefined; + } +}; + +export const castToBooleanIfIsBooleanString = (value: string) => { + if (value === "true") { + return true; + } + + if (value === "false") { + return false; + } + + return value; +}; + +export const isSafari = + /Safari/.test(navigator.userAgent) && /Apple Computer/.test(navigator.vendor); + +/** + * Convert time from seconds to d:h:m:s + * Ex: 70 seconds = 1m 10s + * @param timeInSeconds + */ +export const calculateTimeFromMillis = (timeInSeconds: number) => { + let totalTime = timeInSeconds >= 0 ? timeInSeconds : 0; + const perDay = 24 * 60 * 60; + const perHour = 60 * 60; + const perMinute = 60; + + const days = Math.floor(totalTime / perDay); + + if (days > 0) { + totalTime %= days * perDay; + } + + const hours = Math.floor(totalTime / perHour); + + if (hours > 0) { + totalTime %= hours * perHour; + } + + const minutes = Math.floor(totalTime / perMinute); + + if (minutes > 0) { + totalTime %= minutes * perMinute; + } + + if (days === 0) { + if (hours === 0) { + return `${minutes}m ${totalTime}s`; + } else { + return `${hours}h ${minutes}m ${totalTime}s`; + } + } + return `${days}d ${hours}h ${minutes}m ${totalTime}s`; +}; + +export const calculateDifferentTime = (startTime: number, endTime: number) => { + if (endTime >= startTime) { + const executionTime = endTime - startTime; + + if (executionTime < 1000) { + return `${executionTime} ms`; + } + + return calculateTimeFromMillis(Math.floor(executionTime / 1000)); + } + + return ""; +}; + +export const createSearchableTags = (tags: TagDto[]) => { + return (tags || []).map((tag) => `${tag.key}:${tag.value}`).join(" "); +}; + +export const totalPages = ( + currentPage: number, + rowsPerPage: string, + resultLength: string, +) => { + let value = ""; + if (currentPage === 1 && resultLength < rowsPerPage) { + value = "1"; + } else { + value = "many"; + } + return value; +}; + +/** + * Finding the missing number sequentially + * ex: array = [0,1,1,2,2,15] + * expected: missingNum = 3 + * @param arr: number[] + */ +export const findNextMissingSequentialNumber = (arr: number[]) => { + // Step 1: Sort the array + arr.sort((a, b) => a - b); + + // Step 2: Loop through the sorted array and find the missing numbers + let lastNumber = arr[0]; + let nextMissingNumber = null; + + for (let i = 1; i < arr.length; i++) { + const currentNumber = arr[i]; + if (currentNumber !== lastNumber && currentNumber - lastNumber > 1) { + nextMissingNumber = lastNumber + 1; + break; + } + lastNumber = currentNumber; + } + + // Step 3: Return the next missing number + return nextMissingNumber; +}; + +export const useCoerceToObject = ( + onChange: (a: string) => void, + oValue: string | Record, +): [(val: string) => void, string, boolean] => { + const [stringAsObject, setObjString] = useState<[string, boolean]>([ + inferType(oValue) === FIELD_TYPE_OBJECT + ? JSON.stringify(oValue, null, 2) + : "", + false, + ]); + + const handleUpdateObjectValue = useCallback( + (val: string) => { + try { + const parsed = JSON.parse(val); + onChange(parsed); + setObjString([val, false]); + } catch { + setObjString([val, true]); + } + }, + [onChange], + ); + return [handleUpdateObjectValue, ...stringAsObject]; +}; + +export const optionsNameLabelGenerator = (options: string[]) => { + const result: { name: string; label: string }[] = []; + if (options && options.length > 0) { + options.map((item) => { + result.push({ name: item, label: item }); + return item; + }); + } + return result; +}; + +export const extractVariables = (text: string) => { + const regex = /\$\{([^}]+)\}/g; + + const variables = []; + let match; + + while ((match = regex.exec(text)) !== null) { + variables.push(match[1]); + } + + return variables; +}; + +export const capitalizeEachWord = (text: string) => { + if (text.length > 1) { + const words = text.split(" "); + + const capitalizedWords = words.map((word) => { + return word.toLowerCase().charAt(0).toUpperCase() + word.slice(1); + }); + + return capitalizedWords.join(" "); + } else { + return text; + } +}; + +export const isPseudoTask = (task: TaskDef) => + [TaskType.TERMINAL, TaskType.SWITCH_JOIN].includes(task.type); + +export const replaceNonAlphanumericWithUnderscore = (string: string) => { + return string.replace(/[^a-zA-Z0-9_]+/g, "_").replace(/^_+|_+$/g, ""); +}; + +// Utility function to get cookie value +export const getCookie = (name: string): string | null => { + const matches = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`)); + return matches ? decodeURIComponent(matches[1]) : null; +}; + +export const defaultValueFromSchema = (schema?: JsonSchema) => { + if (!schema?.properties) { + return {}; + } + const defaultValues = _mapValues( + schema.properties, + (property) => property?.default, + ); + const sanitizedDefaults = _pickBy(defaultValues, (val) => val !== undefined); + return sanitizedDefaults; +}; + +export const getBaseUrl = (url = "") => { + try { + const parsedUrl = new URL(url); + return `${parsedUrl?.protocol}//${parsedUrl?.hostname}${ + parsedUrl?.port ? `:${parsedUrl?.port}` : "" + }`; + } catch (error) { + console.error("Invalid URL:", error); + return ""; + } +}; + +export const getInitials = (text: string, fallback = "NA"): string => { + if (!text) return fallback; + + const words = text + ?.replace(/[_-]/g, " ") // Replace underscores and hyphens with spaces + ?.replace(/([a-z])([A-Z])/g, "$1 $2") // Split camelCase (e.g., myText -> my Text) + ?.split(" ") + ?.filter(Boolean); // Remove empty strings + + if (words.length === 0) return fallback; + + // Handle single word case + if (words.length === 1) { + const word = words[0].trim(); + return word.length >= 2 + ? word.substring(0, 2).toUpperCase() + : (word[0]?.toUpperCase() || fallback[0]) + (fallback[1] || ""); + } + + // Handle multiple words + const initials = words + ?.slice(0, 2) + ?.map((word) => word[0]?.toUpperCase() || "") + ?.join(""); + + return initials || fallback; +}; diff --git a/ui-next/src/utils/workflow.ts b/ui-next/src/utils/workflow.ts new file mode 100644 index 0000000000..89ae532446 --- /dev/null +++ b/ui-next/src/utils/workflow.ts @@ -0,0 +1,481 @@ +import { WorkflowDef } from "types/WorkflowDef"; +import _sortBy from "lodash/sortBy"; +import _uniqBy from "lodash/fp/uniqBy"; +import _uniq from "lodash/fp/uniq"; +import { + CommonTaskDef, + ForkJoinTaskDef, + SwitchTaskDef, + DoWhileTaskDef, + SetVariableTaskDef, + ForkJoinDynamicDef, +} from "types/TaskType"; +import { + isDynamicForkTask, + isHumanTask, + isJDBCTask, + isLLMTask, + isSetVariable, + isTask, +} from "./task"; +import { isObjectOnlyNotArray, isObjectOrArray } from "./object"; + +/** + * Get unique workflows with latest version + * @param workflows WorkflowDef[] + * @returns WorkflowDef[] + */ +export const getUniqueWorkflows = (workflows: WorkflowDef[]) => { + const unique = new Map(); + const types = new Set(); + + for (const workflowDef of workflows) { + if (!workflowDef.createTime) { + workflowDef.createTime = 0; + } + + if (!unique.has(workflowDef.name)) { + unique.set(workflowDef.name, workflowDef); + } else if (unique.get(workflowDef.name).version < workflowDef.version) { + unique.set(workflowDef.name, workflowDef); + } + + if (workflowDef.tasks) { + for (const task of workflowDef.tasks) { + types.add(task.type); + } + } + } + + return Array.from(unique.values()); +}; + +/** + * Get unique workflows with versions + * @param workflows WorkflowDef[] + * @returns Map + */ +export const getUniqueWorkflowsWithVersions = (workflows?: WorkflowDef[]) => { + const result = new Map(); + + if (workflows) { + for (const def of workflows) { + let arr: number[] = result.get(def.name) || []; + + if (!result.has(def.name)) { + arr = []; + result.set(def.name, arr); + } + + arr.push(def.version); + } + + // Sort arrays in place + result.forEach((val, key) => { + // Sort versions + result.set(key, _sortBy(val)); + }); + } + + return result; +}; + +const isForkJoin = (task: CommonTaskDef): task is ForkJoinTaskDef => + task.type === "FORK_JOIN"; +const isSwitch = (task: CommonTaskDef): task is SwitchTaskDef => + task.type === "SWITCH"; +const isDoWhile = (task: CommonTaskDef): task is DoWhileTaskDef => + task.type === "DO_WHILE"; + +export function mapWalk( + tasks: CommonTaskDef[], + fn: (task: CommonTaskDef) => CommonTaskDef | null, +): CommonTaskDef[] { + return tasks.flatMap((task) => { + const newTask = fn(task); + if (!newTask) { + return []; + } + + if (isForkJoin(newTask)) { + newTask.forkTasks = newTask.forkTasks.map((tasks) => mapWalk(tasks, fn)); + } + + if (isSwitch(newTask)) { + newTask.decisionCases = Object.fromEntries( + Object.entries(newTask.decisionCases).map(([key, tasks]) => [ + key, + mapWalk(tasks, fn), + ]), + ); + if (newTask.defaultCase) { + newTask.defaultCase = mapWalk(newTask.defaultCase, fn); + } + } + + if (isDoWhile(newTask)) { + newTask.loopOver = mapWalk(newTask.loopOver, fn); + } + + return newTask; + }); +} + +function* walk(tasks: CommonTaskDef[]): Generator { + for (const task of tasks) { + yield task; + + if (isForkJoin(task) && task.forkTasks) { + for (const forkBranch of task.forkTasks) { + yield* walk(forkBranch); + } + } + + if (isSwitch(task)) { + if (task.decisionCases) { + for (const caseTasks of Object.values(task.decisionCases)) { + yield* walk(caseTasks); + } + } + if (task.defaultCase) yield* walk(task.defaultCase); + } + + if (isDoWhile(task) && task.loopOver) yield* walk(task.loopOver); + } +} + +// Flatten: returns a flat array of tasks +export function flatten(tasks: CommonTaskDef[]): CommonTaskDef[] { + return [...walk(tasks)]; +} + +export function filterTasks( + tasks: CommonTaskDef[], + predicate: (task: CommonTaskDef) => boolean, +): CommonTaskDef[] { + return Array.from(walk(tasks)).filter(predicate); +} + +/// Task Predicates + +const hasWorkflowVariable = (a: string): boolean => a.includes("${"); +/// Move to utils +// Move to utils + +const variableValueTaskParserExtractor = ( + task: SetVariableTaskDef | ForkJoinDynamicDef, + extractor: (task: CommonTaskDef) => string[], +): string[] => { + const taskValues = Object.values(task.inputParameters); + const valuesThatAreObjectOrArrays = taskValues.filter((value) => + isObjectOrArray(value), + ); + return valuesThatAreObjectOrArrays.flatMap((value) => { + if (isObjectOnlyNotArray(value) && isTask(value)) { + return extractor(value); + } else if (Array.isArray(value)) { + return value.flatMap((v) => extractor(v)); + } + return []; + }); +}; + +export const handlebarsMatcherExtractor = ( + val: any, + matcher: (val: string) => boolean, +): string[] => { + if (val && typeof val === "string") { + if (matcher(val)) { + return [val]; + } + } + if (Array.isArray(val)) { + return val.flatMap((v) => handlebarsMatcherExtractor(v, matcher)); + } + if (isObjectOnlyNotArray(val)) { + return Object.values(val).flatMap((v) => + handlebarsMatcherExtractor(v, matcher), + ); + } + return []; +}; + +const extractIntegrationName = (task: CommonTaskDef): string[] => { + if (isLLMTask(task)) { + if ( + "llmProvider" in task.inputParameters && + task.inputParameters.llmProvider != null && + !hasWorkflowVariable(task.inputParameters.llmProvider) + ) { + return [task.inputParameters.llmProvider]; + } else if ( + "vectorDB" in task.inputParameters && + task.inputParameters.vectorDB != null && + !hasWorkflowVariable(task.inputParameters.vectorDB) + ) { + return [task.inputParameters.vectorDB]; + } + } else if (isSetVariable(task) || isDynamicForkTask(task)) { + return variableValueTaskParserExtractor(task, extractIntegrationName); + } else if ( + isJDBCTask(task) && + "integrationName" in task.inputParameters && + task.inputParameters.integrationName != null && + !hasWorkflowVariable(task.inputParameters.integrationName) + ) { + return [task.inputParameters.integrationName]; + } + return []; +}; + +const extractPromptName = (task: CommonTaskDef): string[] => { + if (isLLMTask(task)) { + if ( + "promptName" in task.inputParameters && + task.inputParameters.promptName != null && + !hasWorkflowVariable(task.inputParameters.promptName) + ) { + return [task.inputParameters.promptName]; + } + + if ( + "instructions" in task.inputParameters && + task.inputParameters.instructions != null && + !hasWorkflowVariable(task.inputParameters.instructions) + ) { + return [task.inputParameters.instructions]; + } + } else if (isSetVariable(task) || isDynamicForkTask(task)) { + return variableValueTaskParserExtractor(task, extractPromptName); + } + return []; +}; + +export type NameVersion = { + name: string; + version?: string; +}; + +const extractUserFormNameVersion = (task: CommonTaskDef): NameVersion[] => { + if (isHumanTask(task)) { + if (task.inputParameters.__humanTaskDefinition.userFormTemplate?.name) { + return [ + { + name: task.inputParameters.__humanTaskDefinition.userFormTemplate + .name, + version: + task.inputParameters.__humanTaskDefinition.userFormTemplate.version?.toString(), + }, + ]; + } + } + return []; +}; + +const secretMatcher = (val: string) => { + return val.includes("${workflow.secrets.") && val.includes("}"); +}; + +const environmentVariablesMatcher = (val: string) => { + return val.includes("${workflow.env.") && val.includes("}"); +}; + +const extractSecrets = (task: CommonTaskDef): string[] => { + return handlebarsMatcherExtractor(task, secretMatcher); +}; + +const extractEnvironmentVariables = (task: CommonTaskDef): string[] => { + return handlebarsMatcherExtractor(task, environmentVariablesMatcher); +}; + +const extractSchemaNameVersion = (task: CommonTaskDef): NameVersion[] => { + const schemas = []; + if (task.taskDefinition?.inputSchema?.name) { + schemas.push({ + name: task.taskDefinition?.inputSchema?.name, + version: task.taskDefinition?.inputSchema?.version?.toString(), + }); + } + if (task.taskDefinition?.outputSchema?.name) { + schemas.push({ + name: task.taskDefinition?.outputSchema?.name, + version: task.taskDefinition?.outputSchema?.version?.toString(), + }); + } + return schemas; +}; + +type FoundDependencies = { + integrationNames: Set; + promptNames: Set; + userFormsNameVersion: NameVersion[]; + schemas: NameVersion[]; + secrets: Set; + env: Set; +}; + +const uniqueNameVersion = _uniqBy( + (nv: NameVersion) => `${nv.name}:${nv.version || ""}`, +); + +/** + * Walks through all available tasks in search for dependencies + * + * @param tasks wokflow tasks + * @returns + */ +export function scanTasksForDependenciesInTasks(tasks: CommonTaskDef[]) { + const extractedDependencies = Array.from( + walk(tasks), + ).reduce( + (acc: FoundDependencies, task): FoundDependencies => { + const integrationNames = extractIntegrationName(task); + if (integrationNames && integrationNames.length > 0) { + integrationNames.forEach((name) => acc.integrationNames.add(name)); + } + const promptNames = extractPromptName(task); + if (promptNames && promptNames.length > 0) { + promptNames.forEach((name) => acc.promptNames.add(name)); + } + + const secrets = extractSecrets(task); + secrets.forEach((s) => acc.secrets.add(s)); + + const env = extractEnvironmentVariables(task); + env.forEach((ev) => acc.env.add(ev)); + + const userForms = extractUserFormNameVersion(task); + + const schemas = extractSchemaNameVersion(task); + + return { + ...acc, + userFormsNameVersion: acc.userFormsNameVersion.concat(userForms), + schemas: acc.schemas.concat(schemas), + }; + }, + { + integrationNames: new Set(), + promptNames: new Set(), + userFormsNameVersion: [], + secrets: new Set(), + schemas: [], + env: new Set(), + }, + ); + return { + integrationNames: Array.from(extractedDependencies.integrationNames), + promptNames: Array.from(extractedDependencies.promptNames), + userFormsNameVersion: uniqueNameVersion( + extractedDependencies.userFormsNameVersion, + ), + schemas: uniqueNameVersion(extractedDependencies.schemas), + secrets: Array.from(extractedDependencies.secrets), + env: Array.from(extractedDependencies.env), + } as const; +} + +export function scanTasksForDependenciesInWorkflow(workflow: WorkflowDef) { + const taskDependencies = scanTasksForDependenciesInTasks( + workflow.tasks || [], + ); + + const workflowSchema: NameVersion[] = []; + + if (workflow.inputSchema?.name) { + workflowSchema.push({ + name: workflow.inputSchema.name as string, + version: workflow.inputSchema?.version?.toString(), + }); + } + + if (workflow.outputSchema?.name) { + workflowSchema.push({ + name: workflow.outputSchema.name as string, + version: workflow.outputSchema?.version?.toString(), + }); + } + + const workflowSecrets = handlebarsMatcherExtractor( + workflow?.outputParameters ?? {}, + secretMatcher, + ); + + const workflowEnv = handlebarsMatcherExtractor( + workflow?.outputParameters ?? {}, + environmentVariablesMatcher, + ); + + return { + ...taskDependencies, + schemas: uniqueNameVersion(taskDependencies.schemas.concat(workflowSchema)), + secrets: _uniq(taskDependencies.secrets.concat(workflowSecrets)), + env: _uniq(taskDependencies.env.concat(workflowEnv)), + workflowName: workflow.name, + workflowVersion: workflow.version, + }; +} + +export const replaceIntegrationName = ( + task: CommonTaskDef, + originalName: string, + replaceName: string, +): CommonTaskDef => { + if (isLLMTask(task)) { + const newInputParameters = { ...task.inputParameters }; + let changed = false; + + if ( + "llmProvider" in newInputParameters && + newInputParameters.llmProvider === originalName && + !hasWorkflowVariable(newInputParameters.llmProvider) + ) { + newInputParameters.llmProvider = replaceName; + changed = true; + } else if ( + "vectorDB" in newInputParameters && + newInputParameters.vectorDB === originalName && + !hasWorkflowVariable(newInputParameters.vectorDB) + ) { + newInputParameters.vectorDB = replaceName; + changed = true; + } + + if (changed) { + return { ...task, inputParameters: newInputParameters } as typeof task; + } + return task; + } else if (isSetVariable(task) || isDynamicForkTask(task)) { + // Recursively replace in inputParameters + const newInputParameters = { ...task.inputParameters }; + let changed = false; + for (const key of Object.keys(newInputParameters)) { + const value = newInputParameters[key as keyof typeof newInputParameters]; + if (isObjectOnlyNotArray(value) && isTask(value)) { + const replaced = replaceIntegrationName( + value, + originalName, + replaceName, + ); + if (replaced !== value) { + newInputParameters[key as keyof typeof newInputParameters] = replaced; + changed = true; + } + } else if (Array.isArray(value)) { + const replacedArr = value.map((v) => + isTask(v) ? replaceIntegrationName(v, originalName, replaceName) : v, + ); + if (JSON.stringify(replacedArr) !== JSON.stringify(value)) { + newInputParameters[key as keyof typeof newInputParameters] = + replacedArr; + changed = true; + } + } + } + if (changed) { + return { ...task, inputParameters: newInputParameters } as typeof task; + } + return task; + } + return task; +}; diff --git a/ui-next/tsconfig.json b/ui-next/tsconfig.json new file mode 100644 index 0000000000..6e9463207b --- /dev/null +++ b/ui-next/tsconfig.json @@ -0,0 +1,39 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "noFallthroughCasesInSwitch": true, + "declaration": true, + "declarationDir": "./dist", + "baseUrl": "src", + "paths": { + "components/*": ["components/*"], + "images/*": ["images/*"], + "pages/*": ["pages/*"], + "plugins/*": ["plugins/*"], + "shared/*": ["shared/*"], + "theme/*": ["theme/*"], + "types/*": ["types/*"], + "utils/*": ["utils/*"], + "commonServices/*": ["commonServices/*"], + "growthbook/*": ["growthbook/*"], + "templates/*": ["templates/*"], + "testData/*": ["testData/*"] + }, + "types": ["react", "react-dom", "vite/client", "vitest/globals", "node"] + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/ui-next/vite-plugin-csp-nonce.ts b/ui-next/vite-plugin-csp-nonce.ts new file mode 100644 index 0000000000..40e6d04b01 --- /dev/null +++ b/ui-next/vite-plugin-csp-nonce.ts @@ -0,0 +1,20 @@ +import type { Plugin } from "vite"; + +const NONCE_VALUE = "tpsHAxwU5x0csoIuLNs2vg=="; + +/** + * Vite plugin to add CSP nonces to all script tags in the built HTML + */ +export function vitePluginCspNonce(): Plugin { + return { + name: "vite-plugin-csp-nonce", + enforce: "post", + transformIndexHtml(html) { + // Add nonce to all script tags that don't already have one + return html.replace( + /]*\snonce=)([^>]*)>/gi, + ``, + ); + }, + }; +} + +// https://vite.dev/config/ +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, packageDir); + const BASE_URL = env.VITE_PUBLIC_URL || "/"; + + // Library build mode - creates npm package + // Note: Type declarations (dts) disabled due to compatibility issues + // Run `tsc --emitDeclarationOnly` separately if needed + if (mode === "lib") { + return { + plugins: [react(), tsconfigPaths(), svgr()], + build: { + lib: { + entry: resolve(__dirname, "src/index.ts"), + name: "ConductorUI", + fileName: "conductor-ui", + formats: ["es"] as const, + }, + rollupOptions: { + external: [ + "react", + "react-dom", + "react/jsx-runtime", + "react-router", + "react-router-dom", + "@mui/material", + "@mui/icons-material", + "@mui/system", + "@mui/x-date-pickers", + "@emotion/react", + "@emotion/styled", + ], + output: { + globals: { + react: "React", + "react-dom": "ReactDOM", + "react-router-dom": "ReactRouterDOM", + }, + }, + }, + sourcemap: true, + }, + }; + } + + // App build mode - creates standalone OSS application + return { + base: BASE_URL, + plugins: [ + react(), + tsconfigPaths(), + svgr(), + vitePluginCspNonce(), + contextJsHashPlugin(), + ], + optimizeDeps: { + include: [ + "@emotion/react", + "@emotion/styled", + "@mui/material", + "@mui/system", + ], + }, + define: { + "process.env": {}, + }, + preview: { + port: 1234, + }, + server: { + port: 1234, + proxy: { + "/api": { + target: env.VITE_WF_SERVER || "http://localhost:8080", + changeOrigin: true, + }, + "/swagger-ui": { + target: env.VITE_WF_SERVER || "http://localhost:8080", + changeOrigin: true, + }, + "/api-docs": { + target: env.VITE_WF_SERVER || "http://localhost:8080", + changeOrigin: true, + }, + }, + }, + build: { + outDir: "dist", + }, + test: { + globals: true, + environment: "jsdom", + setupFiles: "./src/setupTests.ts", + include: ["src/**/*.test.{js,ts,jsx,tsx}"], + server: { + deps: { + // Force Vitest to process Monaco's ESM through its own pipeline + // rather than trying to load browser-only bundles in jsdom. + inline: ["monaco-editor"], + }, + }, + }, + }; +});