From 41a63bde8f4bb5cbf7eb106e5bbb075940bf3022 Mon Sep 17 00:00:00 2001 From: Nik Nasr Date: Sun, 13 Oct 2024 21:10:14 +0100 Subject: [PATCH] Initial commit --- .eslintignore | 2 + .eslintrc.json | 41 + .github/workflows/pr.yml | 67 + .github/workflows/release.yml | 26 + .gitignore | 49 + .npmrc | 4 + .prettierignore | 5 + .prettierrc | 3 + @types/global-env.d.ts | 8 + @types/vite.d.ts | 4 + README.md | 31 + apps/mock-admin-api/.eslintrc.json | 18 + apps/mock-admin-api/project.json | 65 + apps/mock-admin-api/src/main.ts | 15 + apps/mock-admin-api/src/proxy.ts | 38 + apps/mock-admin-api/tsconfig.app.json | 10 + apps/mock-admin-api/tsconfig.json | 14 + apps/web-ui-e2e/.eslintrc.json | 22 + apps/web-ui-e2e/playwright.config.ts | 70 + apps/web-ui-e2e/project.json | 10 + apps/web-ui-e2e/src/example.spec.ts | 9 + apps/web-ui-e2e/tsconfig.json | 19 + apps/web-ui/.eslintignore | 2 + apps/web-ui/.eslintrc.json | 18 + apps/web-ui/.gitignore | 4 + apps/web-ui/README.md | 22 + apps/web-ui/app/entry.client.tsx | 12 + apps/web-ui/app/entry.server.tsx | 19 + apps/web-ui/app/root.tsx | 196 + apps/web-ui/app/routes/_index.tsx | 4 + apps/web-ui/app/routes/overview.tsx | 3 + apps/web-ui/app/tailwind.css | 9 + apps/web-ui/package.json | 27 + apps/web-ui/postcss.config.cjs | 15 + apps/web-ui/project.json | 63 + apps/web-ui/public/android-chrome-192x192.png | Bin 0 -> 8143 bytes apps/web-ui/public/android-chrome-512x512.png | Bin 0 -> 22739 bytes apps/web-ui/public/apple-touch-icon.png | Bin 0 -> 2770 bytes apps/web-ui/public/browserconfig.xml | 9 + apps/web-ui/public/favicon-16x16.png | Bin 0 -> 855 bytes apps/web-ui/public/favicon-32x32.png | Bin 0 -> 1259 bytes apps/web-ui/public/favicon.ico | Bin 0 -> 15086 bytes apps/web-ui/public/mstile-144x144.png | Bin 0 -> 6224 bytes apps/web-ui/public/mstile-150x150.png | Bin 0 -> 6057 bytes apps/web-ui/public/mstile-310x150.png | Bin 0 -> 6619 bytes apps/web-ui/public/mstile-310x310.png | Bin 0 -> 13157 bytes apps/web-ui/public/mstile-70x70.png | Bin 0 -> 4158 bytes apps/web-ui/public/safari-pinned-tab.svg | 33 + apps/web-ui/public/site.webmanifest | 19 + apps/web-ui/remix.env.d.ts | 2 + apps/web-ui/tailwind.config.cjs | 36 + apps/web-ui/test-setup.ts | 3 + apps/web-ui/tests/routes/_index.spec.tsx | 21 + apps/web-ui/tsconfig.app.json | 27 + apps/web-ui/tsconfig.json | 26 + apps/web-ui/tsconfig.spec.json | 30 + apps/web-ui/vite.config.ts | 69 + libs/data-access/admin-api-fixtures/.babelrc | 10 + .../admin-api-fixtures/.eslintrc.json | 18 + libs/data-access/admin-api-fixtures/README.md | 3 + .../admin-api-fixtures/project.json | 9 + .../admin-api-fixtures/src/index.ts | 1 + .../admin-api-fixtures/src/lib/adminApiDb.ts | 52 + .../src/lib/adminApiMockHandlers.ts | 106 + .../admin-api-fixtures/tsconfig.json | 21 + .../admin-api-fixtures/tsconfig.lib.json | 10 + libs/data-access/admin-api/.babelrc | 12 + libs/data-access/admin-api/.eslintrc.json | 18 + libs/data-access/admin-api/README.md | 7 + libs/data-access/admin-api/project.json | 19 + libs/data-access/admin-api/src/api.ts | 4 + libs/data-access/admin-api/src/index.ts | 4 + .../src/lib/AdminBaseUrlProvider.tsx | 23 + .../admin-api/src/lib/api/client.ts | 236 + .../admin-api/src/lib/api/hooks.ts | 79 + .../admin-api/src/lib/api/index.d.ts | 1643 + .../admin-api/src/lib/api/spec.json | 1898 ++ .../data-access/admin-api/src/lib/api/type.ts | 6 + libs/data-access/admin-api/tsconfig.json | 21 + libs/data-access/admin-api/tsconfig.lib.json | 23 + libs/data-access/admin-api/tsconfig.spec.json | 26 + libs/data-access/admin-api/vite.config.ts | 27 + libs/features/overview-route/.babelrc | 12 + libs/features/overview-route/.eslintrc.json | 18 + libs/features/overview-route/README.md | 7 + libs/features/overview-route/project.json | 9 + libs/features/overview-route/src/index.ts | 1 + .../overview-route/src/lib/Deployment.tsx | 61 + .../src/lib/Details.tsx/Deployment.tsx | 29 + .../src/lib/Details.tsx/Service.tsx | 29 + .../RegisterDeployment/AdditionalHeaders.tsx | 97 + .../lib/RegisterDeployment/AssumeARNRole.tsx | 20 + .../src/lib/RegisterDeployment/Dialog.tsx | 82 + .../src/lib/RegisterDeployment/Form.tsx | 258 + .../src/lib/RegisterDeployment/Results.tsx | 79 + .../src/lib/RegisterDeployment/UseHTTP11.tsx | 26 + .../overview-route/src/lib/RestateServer.tsx | 30 + .../overview-route/src/lib/Service.tsx | 28 + .../overview-route/src/lib/overview.route.tsx | 122 + .../overview-route/src/lib/restate-server.svg | 5 + libs/features/overview-route/src/lib/types.ts | 15 + libs/features/overview-route/tsconfig.json | 20 + .../features/overview-route/tsconfig.lib.json | 23 + .../overview-route/tsconfig.spec.json | 26 + libs/features/overview-route/vite.config.ts | 27 + libs/ui/button/.babelrc | 12 + libs/ui/button/.eslintrc.json | 18 + libs/ui/button/README.md | 7 + libs/ui/button/project.json | 9 + libs/ui/button/src/index.ts | 2 + libs/ui/button/src/lib/Button.spec.tsx | 9 + libs/ui/button/src/lib/Button.tsx | 60 + libs/ui/button/src/lib/SubmitButton.tsx | 109 + libs/ui/button/src/test-setup.ts | 3 + libs/ui/button/tsconfig.json | 19 + libs/ui/button/tsconfig.lib.json | 22 + libs/ui/button/tsconfig.spec.json | 26 + libs/ui/button/vite.config.ts | 25 + libs/ui/code/.babelrc | 12 + libs/ui/code/.eslintrc.json | 18 + libs/ui/code/README.md | 7 + libs/ui/code/project.json | 9 + libs/ui/code/src/index.ts | 2 + libs/ui/code/src/lib/Code.tsx | 14 + libs/ui/code/src/lib/Snippet.tsx | 145 + libs/ui/code/src/lib/SyntaxHighlighter.tsx | 13 + libs/ui/code/tsconfig.json | 20 + libs/ui/code/tsconfig.lib.json | 23 + libs/ui/code/tsconfig.spec.json | 26 + libs/ui/code/vite.config.ts | 27 + libs/ui/details/.babelrc | 12 + libs/ui/details/.eslintrc.json | 18 + libs/ui/details/README.md | 7 + libs/ui/details/project.json | 9 + libs/ui/details/src/index.ts | 2 + libs/ui/details/src/lib/Details.tsx | 49 + libs/ui/details/src/lib/DetailsContext.tsx | 18 + libs/ui/details/src/lib/Summary.tsx | 51 + libs/ui/details/tsconfig.json | 20 + libs/ui/details/tsconfig.lib.json | 23 + libs/ui/details/tsconfig.spec.json | 26 + libs/ui/details/vite.config.ts | 27 + libs/ui/dialog/.babelrc | 12 + libs/ui/dialog/.eslintrc.json | 18 + libs/ui/dialog/README.md | 7 + libs/ui/dialog/project.json | 9 + libs/ui/dialog/src/index.ts | 6 + libs/ui/dialog/src/lib/Dialog.tsx | 19 + libs/ui/dialog/src/lib/DialogClose.tsx | 21 + libs/ui/dialog/src/lib/DialogContent.tsx | 61 + libs/ui/dialog/src/lib/DialogFooter.tsx | 41 + libs/ui/dialog/src/lib/DialogTrigger.tsx | 9 + libs/ui/dialog/src/lib/useDialog.ts | 8 + libs/ui/dialog/tsconfig.json | 20 + libs/ui/dialog/tsconfig.lib.json | 23 + libs/ui/dialog/tsconfig.spec.json | 26 + libs/ui/dialog/vite.config.ts | 27 + libs/ui/dropdown/.babelrc | 12 + libs/ui/dropdown/.eslintrc.json | 18 + libs/ui/dropdown/README.md | 7 + libs/ui/dropdown/project.json | 9 + libs/ui/dropdown/src/index.ts | 7 + libs/ui/dropdown/src/lib/Dropdown.tsx | 9 + libs/ui/dropdown/src/lib/DropdownItem.tsx | 113 + libs/ui/dropdown/src/lib/DropdownMenu.tsx | 68 + libs/ui/dropdown/src/lib/DropdownPopover.tsx | 22 + libs/ui/dropdown/src/lib/DropdownSection.tsx | 30 + .../ui/dropdown/src/lib/DropdownSeparator.tsx | 5 + libs/ui/dropdown/src/lib/DropdownTrigger.tsx | 10 + libs/ui/dropdown/tsconfig.json | 20 + libs/ui/dropdown/tsconfig.lib.json | 23 + libs/ui/dropdown/tsconfig.spec.json | 26 + libs/ui/dropdown/vite.config.ts | 27 + libs/ui/error/.babelrc | 12 + libs/ui/error/.eslintrc.json | 18 + libs/ui/error/README.md | 7 + libs/ui/error/project.json | 9 + libs/ui/error/src/index.ts | 3 + libs/ui/error/src/lib/CrashError.tsx | 31 + libs/ui/error/src/lib/ErrorBanner.tsx | 98 + libs/ui/error/src/lib/InlineError.tsx | 25 + libs/ui/error/tsconfig.json | 20 + libs/ui/error/tsconfig.lib.json | 24 + libs/ui/error/tsconfig.spec.json | 26 + libs/ui/error/vite.config.ts | 27 + libs/ui/focus/.babelrc | 12 + libs/ui/focus/.eslintrc.json | 18 + libs/ui/focus/README.md | 7 + libs/ui/focus/project.json | 9 + libs/ui/focus/src/index.ts | 1 + libs/ui/focus/src/lib/focus.tsx | 11 + libs/ui/focus/tsconfig.json | 20 + libs/ui/focus/tsconfig.lib.json | 23 + libs/ui/focus/tsconfig.spec.json | 26 + libs/ui/focus/vite.config.ts | 27 + libs/ui/form-field/.babelrc | 12 + libs/ui/form-field/.eslintrc.json | 18 + libs/ui/form-field/README.md | 7 + libs/ui/form-field/project.json | 9 + libs/ui/form-field/src/index.ts | 7 + .../form-field/src/lib/FormFieldCheckbox.tsx | 72 + libs/ui/form-field/src/lib/FormFieldError.tsx | 16 + libs/ui/form-field/src/lib/FormFieldGroup.tsx | 43 + libs/ui/form-field/src/lib/FormFieldInput.tsx | 71 + libs/ui/form-field/src/lib/FormFieldLabel.tsx | 14 + .../ui/form-field/src/lib/FormFieldSelect.tsx | 87 + .../form-field/src/lib/FormFieldTextarea.tsx | 61 + libs/ui/form-field/tsconfig.json | 20 + libs/ui/form-field/tsconfig.lib.json | 23 + libs/ui/form-field/tsconfig.spec.json | 26 + libs/ui/form-field/vite.config.ts | 27 + libs/ui/icons/.babelrc | 12 + libs/ui/icons/.eslintrc.json | 18 + libs/ui/icons/README.md | 7 + libs/ui/icons/project.json | 9 + libs/ui/icons/src/index.ts | 1 + libs/ui/icons/src/lib/Icons.tsx | 135 + .../ui/icons/src/lib/custom-icons/CircleX.tsx | 21 + .../ui/icons/src/lib/custom-icons/Discord.tsx | 23 + libs/ui/icons/src/lib/custom-icons/Docs.tsx | 23 + libs/ui/icons/src/lib/custom-icons/Github.tsx | 23 + libs/ui/icons/src/lib/custom-icons/Help.tsx | 21 + libs/ui/icons/src/lib/custom-icons/Lambda.tsx | 23 + .../ui/icons/src/lib/custom-icons/Restate.tsx | 23 + .../lib/custom-icons/RestateEnvironment.tsx | 23 + .../src/lib/custom-icons/SupportTicket.tsx | 21 + libs/ui/icons/tsconfig.json | 20 + libs/ui/icons/tsconfig.lib.json | 23 + libs/ui/icons/tsconfig.spec.json | 26 + libs/ui/icons/vite.config.ts | 27 + libs/ui/layout/.babelrc | 12 + libs/ui/layout/.eslintrc.json | 18 + libs/ui/layout/README.md | 7 + libs/ui/layout/project.json | 9 + libs/ui/layout/src/index.ts | 8 + libs/ui/layout/src/lib/AppBar.module.css | 12 + libs/ui/layout/src/lib/AppBar.tsx | 21 + libs/ui/layout/src/lib/Complementary.tsx | 101 + .../ui/layout/src/lib/ComplementaryOutlet.tsx | 21 + libs/ui/layout/src/lib/Layout.tsx | 47 + libs/ui/layout/src/lib/LayoutZone.ts | 15 + libs/ui/layout/src/lib/Notification.tsx | 18 + libs/ui/layout/tsconfig.json | 20 + libs/ui/layout/tsconfig.lib.json | 23 + libs/ui/layout/tsconfig.spec.json | 26 + libs/ui/layout/vite.config.ts | 27 + libs/ui/link/.babelrc | 12 + libs/ui/link/.eslintrc.json | 18 + libs/ui/link/README.md | 7 + libs/ui/link/project.json | 9 + libs/ui/link/src/index.ts | 1 + libs/ui/link/src/lib/Link.tsx | 49 + libs/ui/link/tsconfig.json | 20 + libs/ui/link/tsconfig.lib.json | 23 + libs/ui/link/tsconfig.spec.json | 26 + libs/ui/link/vite.config.ts | 27 + libs/ui/listbox/.babelrc | 12 + libs/ui/listbox/.eslintrc.json | 18 + libs/ui/listbox/README.md | 7 + libs/ui/listbox/project.json | 9 + libs/ui/listbox/src/index.ts | 4 + libs/ui/listbox/src/lib/ListBox.tsx | 56 + libs/ui/listbox/src/lib/ListBoxItem.tsx | 108 + libs/ui/listbox/src/lib/ListBoxSection.tsx | 17 + libs/ui/listbox/tsconfig.json | 20 + libs/ui/listbox/tsconfig.lib.json | 23 + libs/ui/listbox/tsconfig.spec.json | 26 + libs/ui/listbox/vite.config.ts | 27 + libs/ui/nav/.babelrc | 12 + libs/ui/nav/.eslintrc.json | 18 + libs/ui/nav/README.md | 7 + libs/ui/nav/project.json | 9 + libs/ui/nav/src/index.ts | 2 + libs/ui/nav/src/lib/Nav.tsx | 86 + libs/ui/nav/src/lib/NavContext.tsx | 7 + libs/ui/nav/src/lib/NavItem.tsx | 126 + libs/ui/nav/tsconfig.json | 20 + libs/ui/nav/tsconfig.lib.json | 23 + libs/ui/nav/tsconfig.spec.json | 26 + libs/ui/nav/vite.config.ts | 27 + libs/ui/popover/.babelrc | 12 + libs/ui/popover/.eslintrc.json | 18 + libs/ui/popover/README.md | 7 + libs/ui/popover/project.json | 9 + libs/ui/popover/src/index.ts | 5 + libs/ui/popover/src/lib/Popover.tsx | 6 + libs/ui/popover/src/lib/PopoverContent.tsx | 17 + libs/ui/popover/src/lib/PopoverOverlay.tsx | 36 + libs/ui/popover/src/lib/PopoverTrigger.tsx | 7 + libs/ui/popover/src/lib/usePopover.ts | 8 + libs/ui/popover/tsconfig.json | 20 + libs/ui/popover/tsconfig.lib.json | 23 + libs/ui/popover/tsconfig.spec.json | 26 + libs/ui/popover/vite.config.ts | 27 + libs/ui/radio-group/.babelrc | 12 + libs/ui/radio-group/.eslintrc.json | 18 + libs/ui/radio-group/README.md | 7 + libs/ui/radio-group/project.json | 9 + libs/ui/radio-group/src/index.ts | 2 + libs/ui/radio-group/src/lib/Radio.tsx | 49 + libs/ui/radio-group/src/lib/RadioGroup.tsx | 37 + libs/ui/radio-group/tsconfig.json | 20 + libs/ui/radio-group/tsconfig.lib.json | 23 + libs/ui/radio-group/tsconfig.spec.json | 26 + libs/ui/radio-group/vite.config.ts | 27 + libs/ui/section/.babelrc | 12 + libs/ui/section/.eslintrc.json | 18 + libs/ui/section/README.md | 7 + libs/ui/section/project.json | 9 + libs/ui/section/src/index.ts | 1 + libs/ui/section/src/lib/Section.tsx | 40 + libs/ui/section/tsconfig.json | 20 + libs/ui/section/tsconfig.lib.json | 23 + libs/ui/section/tsconfig.spec.json | 26 + libs/ui/section/vite.config.ts | 27 + libs/util/errors/.babelrc | 12 + libs/util/errors/.eslintrc.json | 18 + libs/util/errors/README.md | 7 + libs/util/errors/project.json | 9 + libs/util/errors/src/index.ts | 1 + libs/util/errors/src/lib/UnauthorizedError.ts | 5 + libs/util/errors/tsconfig.json | 20 + libs/util/errors/tsconfig.lib.json | 23 + libs/util/errors/tsconfig.spec.json | 26 + libs/util/errors/vite.config.ts | 27 + libs/util/feature-flag/.babelrc | 12 + libs/util/feature-flag/.eslintrc.json | 18 + libs/util/feature-flag/README.md | 7 + libs/util/feature-flag/project.json | 9 + libs/util/feature-flag/src/index.ts | 2 + .../util/feature-flag/src/lib/FeatureFlag.tsx | 13 + .../src/lib/hasAccessToFeature.ts | 3 + libs/util/feature-flag/src/lib/type.ts | 1 + .../feature-flag/src/lib/withFeatureFlag.ts | 23 + libs/util/feature-flag/tsconfig.json | 20 + libs/util/feature-flag/tsconfig.lib.json | 23 + libs/util/feature-flag/tsconfig.spec.json | 26 + libs/util/feature-flag/vite.config.ts | 27 + libs/util/mock-api/.babelrc | 10 + libs/util/mock-api/.eslintrc.json | 18 + libs/util/mock-api/README.md | 14 + libs/util/mock-api/project.json | 9 + libs/util/mock-api/src/index.ts | 1 + libs/util/mock-api/src/lib/enableMockApi.ts | 15 + .../mock-api/src/lib/mockServiceWorker.d.ts | 1 + libs/util/mock-api/tsconfig.json | 19 + libs/util/mock-api/tsconfig.lib.json | 10 + libs/util/playwright/.eslintrc.json | 18 + libs/util/playwright/README.md | 3 + libs/util/playwright/project.json | 9 + libs/util/playwright/src/index.ts | 0 libs/util/playwright/tsconfig.json | 19 + libs/util/playwright/tsconfig.lib.json | 10 + libs/util/react-query/.babelrc | 12 + libs/util/react-query/.eslintrc.json | 18 + libs/util/react-query/README.md | 7 + libs/util/react-query/project.json | 9 + libs/util/react-query/src/index.ts | 1 + .../react-query/src/lib/QueryProvider.tsx | 45 + libs/util/react-query/tsconfig.json | 20 + libs/util/react-query/tsconfig.lib.json | 23 + libs/util/react-query/tsconfig.spec.json | 26 + libs/util/react-query/vite.config.ts | 27 + libs/util/remix/.babelrc | 12 + libs/util/remix/.eslintrc.json | 18 + libs/util/remix/README.md | 7 + libs/util/remix/project.json | 9 + libs/util/remix/src/index.ts | 1 + .../remix/src/lib/useFetcherWithErrors.ts | 33 + libs/util/remix/tsconfig.json | 20 + libs/util/remix/tsconfig.lib.json | 23 + libs/util/remix/tsconfig.spec.json | 26 + libs/util/remix/vite.config.ts | 27 + nx.json | 77 + package.json | 124 + pnpm-lock.yaml | 24785 ++++++++++++++++ scripts/typecheck.js | 20 + tsconfig.base.json | 58 + vitest.workspace.ts | 1 + 379 files changed, 37375 insertions(+) create mode 100644 .eslintignore create mode 100644 .eslintrc.json create mode 100644 .github/workflows/pr.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 @types/global-env.d.ts create mode 100644 @types/vite.d.ts create mode 100644 README.md create mode 100644 apps/mock-admin-api/.eslintrc.json create mode 100644 apps/mock-admin-api/project.json create mode 100644 apps/mock-admin-api/src/main.ts create mode 100644 apps/mock-admin-api/src/proxy.ts create mode 100644 apps/mock-admin-api/tsconfig.app.json create mode 100644 apps/mock-admin-api/tsconfig.json create mode 100644 apps/web-ui-e2e/.eslintrc.json create mode 100644 apps/web-ui-e2e/playwright.config.ts create mode 100644 apps/web-ui-e2e/project.json create mode 100644 apps/web-ui-e2e/src/example.spec.ts create mode 100644 apps/web-ui-e2e/tsconfig.json create mode 100644 apps/web-ui/.eslintignore create mode 100644 apps/web-ui/.eslintrc.json create mode 100644 apps/web-ui/.gitignore create mode 100644 apps/web-ui/README.md create mode 100644 apps/web-ui/app/entry.client.tsx create mode 100644 apps/web-ui/app/entry.server.tsx create mode 100644 apps/web-ui/app/root.tsx create mode 100644 apps/web-ui/app/routes/_index.tsx create mode 100644 apps/web-ui/app/routes/overview.tsx create mode 100644 apps/web-ui/app/tailwind.css create mode 100644 apps/web-ui/package.json create mode 100644 apps/web-ui/postcss.config.cjs create mode 100644 apps/web-ui/project.json create mode 100644 apps/web-ui/public/android-chrome-192x192.png create mode 100644 apps/web-ui/public/android-chrome-512x512.png create mode 100644 apps/web-ui/public/apple-touch-icon.png create mode 100644 apps/web-ui/public/browserconfig.xml create mode 100644 apps/web-ui/public/favicon-16x16.png create mode 100644 apps/web-ui/public/favicon-32x32.png create mode 100644 apps/web-ui/public/favicon.ico create mode 100644 apps/web-ui/public/mstile-144x144.png create mode 100644 apps/web-ui/public/mstile-150x150.png create mode 100644 apps/web-ui/public/mstile-310x150.png create mode 100644 apps/web-ui/public/mstile-310x310.png create mode 100644 apps/web-ui/public/mstile-70x70.png create mode 100644 apps/web-ui/public/safari-pinned-tab.svg create mode 100644 apps/web-ui/public/site.webmanifest create mode 100644 apps/web-ui/remix.env.d.ts create mode 100644 apps/web-ui/tailwind.config.cjs create mode 100644 apps/web-ui/test-setup.ts create mode 100644 apps/web-ui/tests/routes/_index.spec.tsx create mode 100644 apps/web-ui/tsconfig.app.json create mode 100644 apps/web-ui/tsconfig.json create mode 100644 apps/web-ui/tsconfig.spec.json create mode 100644 apps/web-ui/vite.config.ts create mode 100644 libs/data-access/admin-api-fixtures/.babelrc create mode 100644 libs/data-access/admin-api-fixtures/.eslintrc.json create mode 100644 libs/data-access/admin-api-fixtures/README.md create mode 100644 libs/data-access/admin-api-fixtures/project.json create mode 100644 libs/data-access/admin-api-fixtures/src/index.ts create mode 100644 libs/data-access/admin-api-fixtures/src/lib/adminApiDb.ts create mode 100644 libs/data-access/admin-api-fixtures/src/lib/adminApiMockHandlers.ts create mode 100644 libs/data-access/admin-api-fixtures/tsconfig.json create mode 100644 libs/data-access/admin-api-fixtures/tsconfig.lib.json create mode 100644 libs/data-access/admin-api/.babelrc create mode 100644 libs/data-access/admin-api/.eslintrc.json create mode 100644 libs/data-access/admin-api/README.md create mode 100644 libs/data-access/admin-api/project.json create mode 100644 libs/data-access/admin-api/src/api.ts create mode 100644 libs/data-access/admin-api/src/index.ts create mode 100644 libs/data-access/admin-api/src/lib/AdminBaseUrlProvider.tsx create mode 100644 libs/data-access/admin-api/src/lib/api/client.ts create mode 100644 libs/data-access/admin-api/src/lib/api/hooks.ts create mode 100644 libs/data-access/admin-api/src/lib/api/index.d.ts create mode 100644 libs/data-access/admin-api/src/lib/api/spec.json create mode 100644 libs/data-access/admin-api/src/lib/api/type.ts create mode 100644 libs/data-access/admin-api/tsconfig.json create mode 100644 libs/data-access/admin-api/tsconfig.lib.json create mode 100644 libs/data-access/admin-api/tsconfig.spec.json create mode 100644 libs/data-access/admin-api/vite.config.ts create mode 100644 libs/features/overview-route/.babelrc create mode 100644 libs/features/overview-route/.eslintrc.json create mode 100644 libs/features/overview-route/README.md create mode 100644 libs/features/overview-route/project.json create mode 100644 libs/features/overview-route/src/index.ts create mode 100644 libs/features/overview-route/src/lib/Deployment.tsx create mode 100644 libs/features/overview-route/src/lib/Details.tsx/Deployment.tsx create mode 100644 libs/features/overview-route/src/lib/Details.tsx/Service.tsx create mode 100644 libs/features/overview-route/src/lib/RegisterDeployment/AdditionalHeaders.tsx create mode 100644 libs/features/overview-route/src/lib/RegisterDeployment/AssumeARNRole.tsx create mode 100644 libs/features/overview-route/src/lib/RegisterDeployment/Dialog.tsx create mode 100644 libs/features/overview-route/src/lib/RegisterDeployment/Form.tsx create mode 100644 libs/features/overview-route/src/lib/RegisterDeployment/Results.tsx create mode 100644 libs/features/overview-route/src/lib/RegisterDeployment/UseHTTP11.tsx create mode 100644 libs/features/overview-route/src/lib/RestateServer.tsx create mode 100644 libs/features/overview-route/src/lib/Service.tsx create mode 100644 libs/features/overview-route/src/lib/overview.route.tsx create mode 100644 libs/features/overview-route/src/lib/restate-server.svg create mode 100644 libs/features/overview-route/src/lib/types.ts create mode 100644 libs/features/overview-route/tsconfig.json create mode 100644 libs/features/overview-route/tsconfig.lib.json create mode 100644 libs/features/overview-route/tsconfig.spec.json create mode 100644 libs/features/overview-route/vite.config.ts create mode 100644 libs/ui/button/.babelrc create mode 100644 libs/ui/button/.eslintrc.json create mode 100644 libs/ui/button/README.md create mode 100644 libs/ui/button/project.json create mode 100644 libs/ui/button/src/index.ts create mode 100644 libs/ui/button/src/lib/Button.spec.tsx create mode 100644 libs/ui/button/src/lib/Button.tsx create mode 100644 libs/ui/button/src/lib/SubmitButton.tsx create mode 100644 libs/ui/button/src/test-setup.ts create mode 100644 libs/ui/button/tsconfig.json create mode 100644 libs/ui/button/tsconfig.lib.json create mode 100644 libs/ui/button/tsconfig.spec.json create mode 100644 libs/ui/button/vite.config.ts create mode 100644 libs/ui/code/.babelrc create mode 100644 libs/ui/code/.eslintrc.json create mode 100644 libs/ui/code/README.md create mode 100644 libs/ui/code/project.json create mode 100644 libs/ui/code/src/index.ts create mode 100644 libs/ui/code/src/lib/Code.tsx create mode 100644 libs/ui/code/src/lib/Snippet.tsx create mode 100644 libs/ui/code/src/lib/SyntaxHighlighter.tsx create mode 100644 libs/ui/code/tsconfig.json create mode 100644 libs/ui/code/tsconfig.lib.json create mode 100644 libs/ui/code/tsconfig.spec.json create mode 100644 libs/ui/code/vite.config.ts create mode 100644 libs/ui/details/.babelrc create mode 100644 libs/ui/details/.eslintrc.json create mode 100644 libs/ui/details/README.md create mode 100644 libs/ui/details/project.json create mode 100644 libs/ui/details/src/index.ts create mode 100644 libs/ui/details/src/lib/Details.tsx create mode 100644 libs/ui/details/src/lib/DetailsContext.tsx create mode 100644 libs/ui/details/src/lib/Summary.tsx create mode 100644 libs/ui/details/tsconfig.json create mode 100644 libs/ui/details/tsconfig.lib.json create mode 100644 libs/ui/details/tsconfig.spec.json create mode 100644 libs/ui/details/vite.config.ts create mode 100644 libs/ui/dialog/.babelrc create mode 100644 libs/ui/dialog/.eslintrc.json create mode 100644 libs/ui/dialog/README.md create mode 100644 libs/ui/dialog/project.json create mode 100644 libs/ui/dialog/src/index.ts create mode 100644 libs/ui/dialog/src/lib/Dialog.tsx create mode 100644 libs/ui/dialog/src/lib/DialogClose.tsx create mode 100644 libs/ui/dialog/src/lib/DialogContent.tsx create mode 100644 libs/ui/dialog/src/lib/DialogFooter.tsx create mode 100644 libs/ui/dialog/src/lib/DialogTrigger.tsx create mode 100644 libs/ui/dialog/src/lib/useDialog.ts create mode 100644 libs/ui/dialog/tsconfig.json create mode 100644 libs/ui/dialog/tsconfig.lib.json create mode 100644 libs/ui/dialog/tsconfig.spec.json create mode 100644 libs/ui/dialog/vite.config.ts create mode 100644 libs/ui/dropdown/.babelrc create mode 100644 libs/ui/dropdown/.eslintrc.json create mode 100644 libs/ui/dropdown/README.md create mode 100644 libs/ui/dropdown/project.json create mode 100644 libs/ui/dropdown/src/index.ts create mode 100644 libs/ui/dropdown/src/lib/Dropdown.tsx create mode 100644 libs/ui/dropdown/src/lib/DropdownItem.tsx create mode 100644 libs/ui/dropdown/src/lib/DropdownMenu.tsx create mode 100644 libs/ui/dropdown/src/lib/DropdownPopover.tsx create mode 100644 libs/ui/dropdown/src/lib/DropdownSection.tsx create mode 100644 libs/ui/dropdown/src/lib/DropdownSeparator.tsx create mode 100644 libs/ui/dropdown/src/lib/DropdownTrigger.tsx create mode 100644 libs/ui/dropdown/tsconfig.json create mode 100644 libs/ui/dropdown/tsconfig.lib.json create mode 100644 libs/ui/dropdown/tsconfig.spec.json create mode 100644 libs/ui/dropdown/vite.config.ts create mode 100644 libs/ui/error/.babelrc create mode 100644 libs/ui/error/.eslintrc.json create mode 100644 libs/ui/error/README.md create mode 100644 libs/ui/error/project.json create mode 100644 libs/ui/error/src/index.ts create mode 100644 libs/ui/error/src/lib/CrashError.tsx create mode 100644 libs/ui/error/src/lib/ErrorBanner.tsx create mode 100644 libs/ui/error/src/lib/InlineError.tsx create mode 100644 libs/ui/error/tsconfig.json create mode 100644 libs/ui/error/tsconfig.lib.json create mode 100644 libs/ui/error/tsconfig.spec.json create mode 100644 libs/ui/error/vite.config.ts create mode 100644 libs/ui/focus/.babelrc create mode 100644 libs/ui/focus/.eslintrc.json create mode 100644 libs/ui/focus/README.md create mode 100644 libs/ui/focus/project.json create mode 100644 libs/ui/focus/src/index.ts create mode 100644 libs/ui/focus/src/lib/focus.tsx create mode 100644 libs/ui/focus/tsconfig.json create mode 100644 libs/ui/focus/tsconfig.lib.json create mode 100644 libs/ui/focus/tsconfig.spec.json create mode 100644 libs/ui/focus/vite.config.ts create mode 100644 libs/ui/form-field/.babelrc create mode 100644 libs/ui/form-field/.eslintrc.json create mode 100644 libs/ui/form-field/README.md create mode 100644 libs/ui/form-field/project.json create mode 100644 libs/ui/form-field/src/index.ts create mode 100644 libs/ui/form-field/src/lib/FormFieldCheckbox.tsx create mode 100644 libs/ui/form-field/src/lib/FormFieldError.tsx create mode 100644 libs/ui/form-field/src/lib/FormFieldGroup.tsx create mode 100644 libs/ui/form-field/src/lib/FormFieldInput.tsx create mode 100644 libs/ui/form-field/src/lib/FormFieldLabel.tsx create mode 100644 libs/ui/form-field/src/lib/FormFieldSelect.tsx create mode 100644 libs/ui/form-field/src/lib/FormFieldTextarea.tsx create mode 100644 libs/ui/form-field/tsconfig.json create mode 100644 libs/ui/form-field/tsconfig.lib.json create mode 100644 libs/ui/form-field/tsconfig.spec.json create mode 100644 libs/ui/form-field/vite.config.ts create mode 100644 libs/ui/icons/.babelrc create mode 100644 libs/ui/icons/.eslintrc.json create mode 100644 libs/ui/icons/README.md create mode 100644 libs/ui/icons/project.json create mode 100644 libs/ui/icons/src/index.ts create mode 100644 libs/ui/icons/src/lib/Icons.tsx create mode 100644 libs/ui/icons/src/lib/custom-icons/CircleX.tsx create mode 100644 libs/ui/icons/src/lib/custom-icons/Discord.tsx create mode 100644 libs/ui/icons/src/lib/custom-icons/Docs.tsx create mode 100644 libs/ui/icons/src/lib/custom-icons/Github.tsx create mode 100644 libs/ui/icons/src/lib/custom-icons/Help.tsx create mode 100644 libs/ui/icons/src/lib/custom-icons/Lambda.tsx create mode 100644 libs/ui/icons/src/lib/custom-icons/Restate.tsx create mode 100644 libs/ui/icons/src/lib/custom-icons/RestateEnvironment.tsx create mode 100644 libs/ui/icons/src/lib/custom-icons/SupportTicket.tsx create mode 100644 libs/ui/icons/tsconfig.json create mode 100644 libs/ui/icons/tsconfig.lib.json create mode 100644 libs/ui/icons/tsconfig.spec.json create mode 100644 libs/ui/icons/vite.config.ts create mode 100644 libs/ui/layout/.babelrc create mode 100644 libs/ui/layout/.eslintrc.json create mode 100644 libs/ui/layout/README.md create mode 100644 libs/ui/layout/project.json create mode 100644 libs/ui/layout/src/index.ts create mode 100644 libs/ui/layout/src/lib/AppBar.module.css create mode 100644 libs/ui/layout/src/lib/AppBar.tsx create mode 100644 libs/ui/layout/src/lib/Complementary.tsx create mode 100644 libs/ui/layout/src/lib/ComplementaryOutlet.tsx create mode 100644 libs/ui/layout/src/lib/Layout.tsx create mode 100644 libs/ui/layout/src/lib/LayoutZone.ts create mode 100644 libs/ui/layout/src/lib/Notification.tsx create mode 100644 libs/ui/layout/tsconfig.json create mode 100644 libs/ui/layout/tsconfig.lib.json create mode 100644 libs/ui/layout/tsconfig.spec.json create mode 100644 libs/ui/layout/vite.config.ts create mode 100644 libs/ui/link/.babelrc create mode 100644 libs/ui/link/.eslintrc.json create mode 100644 libs/ui/link/README.md create mode 100644 libs/ui/link/project.json create mode 100644 libs/ui/link/src/index.ts create mode 100644 libs/ui/link/src/lib/Link.tsx create mode 100644 libs/ui/link/tsconfig.json create mode 100644 libs/ui/link/tsconfig.lib.json create mode 100644 libs/ui/link/tsconfig.spec.json create mode 100644 libs/ui/link/vite.config.ts create mode 100644 libs/ui/listbox/.babelrc create mode 100644 libs/ui/listbox/.eslintrc.json create mode 100644 libs/ui/listbox/README.md create mode 100644 libs/ui/listbox/project.json create mode 100644 libs/ui/listbox/src/index.ts create mode 100644 libs/ui/listbox/src/lib/ListBox.tsx create mode 100644 libs/ui/listbox/src/lib/ListBoxItem.tsx create mode 100644 libs/ui/listbox/src/lib/ListBoxSection.tsx create mode 100644 libs/ui/listbox/tsconfig.json create mode 100644 libs/ui/listbox/tsconfig.lib.json create mode 100644 libs/ui/listbox/tsconfig.spec.json create mode 100644 libs/ui/listbox/vite.config.ts create mode 100644 libs/ui/nav/.babelrc create mode 100644 libs/ui/nav/.eslintrc.json create mode 100644 libs/ui/nav/README.md create mode 100644 libs/ui/nav/project.json create mode 100644 libs/ui/nav/src/index.ts create mode 100644 libs/ui/nav/src/lib/Nav.tsx create mode 100644 libs/ui/nav/src/lib/NavContext.tsx create mode 100644 libs/ui/nav/src/lib/NavItem.tsx create mode 100644 libs/ui/nav/tsconfig.json create mode 100644 libs/ui/nav/tsconfig.lib.json create mode 100644 libs/ui/nav/tsconfig.spec.json create mode 100644 libs/ui/nav/vite.config.ts create mode 100644 libs/ui/popover/.babelrc create mode 100644 libs/ui/popover/.eslintrc.json create mode 100644 libs/ui/popover/README.md create mode 100644 libs/ui/popover/project.json create mode 100644 libs/ui/popover/src/index.ts create mode 100644 libs/ui/popover/src/lib/Popover.tsx create mode 100644 libs/ui/popover/src/lib/PopoverContent.tsx create mode 100644 libs/ui/popover/src/lib/PopoverOverlay.tsx create mode 100644 libs/ui/popover/src/lib/PopoverTrigger.tsx create mode 100644 libs/ui/popover/src/lib/usePopover.ts create mode 100644 libs/ui/popover/tsconfig.json create mode 100644 libs/ui/popover/tsconfig.lib.json create mode 100644 libs/ui/popover/tsconfig.spec.json create mode 100644 libs/ui/popover/vite.config.ts create mode 100644 libs/ui/radio-group/.babelrc create mode 100644 libs/ui/radio-group/.eslintrc.json create mode 100644 libs/ui/radio-group/README.md create mode 100644 libs/ui/radio-group/project.json create mode 100644 libs/ui/radio-group/src/index.ts create mode 100644 libs/ui/radio-group/src/lib/Radio.tsx create mode 100644 libs/ui/radio-group/src/lib/RadioGroup.tsx create mode 100644 libs/ui/radio-group/tsconfig.json create mode 100644 libs/ui/radio-group/tsconfig.lib.json create mode 100644 libs/ui/radio-group/tsconfig.spec.json create mode 100644 libs/ui/radio-group/vite.config.ts create mode 100644 libs/ui/section/.babelrc create mode 100644 libs/ui/section/.eslintrc.json create mode 100644 libs/ui/section/README.md create mode 100644 libs/ui/section/project.json create mode 100644 libs/ui/section/src/index.ts create mode 100644 libs/ui/section/src/lib/Section.tsx create mode 100644 libs/ui/section/tsconfig.json create mode 100644 libs/ui/section/tsconfig.lib.json create mode 100644 libs/ui/section/tsconfig.spec.json create mode 100644 libs/ui/section/vite.config.ts create mode 100644 libs/util/errors/.babelrc create mode 100644 libs/util/errors/.eslintrc.json create mode 100644 libs/util/errors/README.md create mode 100644 libs/util/errors/project.json create mode 100644 libs/util/errors/src/index.ts create mode 100644 libs/util/errors/src/lib/UnauthorizedError.ts create mode 100644 libs/util/errors/tsconfig.json create mode 100644 libs/util/errors/tsconfig.lib.json create mode 100644 libs/util/errors/tsconfig.spec.json create mode 100644 libs/util/errors/vite.config.ts create mode 100644 libs/util/feature-flag/.babelrc create mode 100644 libs/util/feature-flag/.eslintrc.json create mode 100644 libs/util/feature-flag/README.md create mode 100644 libs/util/feature-flag/project.json create mode 100644 libs/util/feature-flag/src/index.ts create mode 100644 libs/util/feature-flag/src/lib/FeatureFlag.tsx create mode 100644 libs/util/feature-flag/src/lib/hasAccessToFeature.ts create mode 100644 libs/util/feature-flag/src/lib/type.ts create mode 100644 libs/util/feature-flag/src/lib/withFeatureFlag.ts create mode 100644 libs/util/feature-flag/tsconfig.json create mode 100644 libs/util/feature-flag/tsconfig.lib.json create mode 100644 libs/util/feature-flag/tsconfig.spec.json create mode 100644 libs/util/feature-flag/vite.config.ts create mode 100644 libs/util/mock-api/.babelrc create mode 100644 libs/util/mock-api/.eslintrc.json create mode 100644 libs/util/mock-api/README.md create mode 100644 libs/util/mock-api/project.json create mode 100644 libs/util/mock-api/src/index.ts create mode 100644 libs/util/mock-api/src/lib/enableMockApi.ts create mode 100644 libs/util/mock-api/src/lib/mockServiceWorker.d.ts create mode 100644 libs/util/mock-api/tsconfig.json create mode 100644 libs/util/mock-api/tsconfig.lib.json create mode 100644 libs/util/playwright/.eslintrc.json create mode 100644 libs/util/playwright/README.md create mode 100644 libs/util/playwright/project.json create mode 100644 libs/util/playwright/src/index.ts create mode 100644 libs/util/playwright/tsconfig.json create mode 100644 libs/util/playwright/tsconfig.lib.json create mode 100644 libs/util/react-query/.babelrc create mode 100644 libs/util/react-query/.eslintrc.json create mode 100644 libs/util/react-query/README.md create mode 100644 libs/util/react-query/project.json create mode 100644 libs/util/react-query/src/index.ts create mode 100644 libs/util/react-query/src/lib/QueryProvider.tsx create mode 100644 libs/util/react-query/tsconfig.json create mode 100644 libs/util/react-query/tsconfig.lib.json create mode 100644 libs/util/react-query/tsconfig.spec.json create mode 100644 libs/util/react-query/vite.config.ts create mode 100644 libs/util/remix/.babelrc create mode 100644 libs/util/remix/.eslintrc.json create mode 100644 libs/util/remix/README.md create mode 100644 libs/util/remix/project.json create mode 100644 libs/util/remix/src/index.ts create mode 100644 libs/util/remix/src/lib/useFetcherWithErrors.ts create mode 100644 libs/util/remix/tsconfig.json create mode 100644 libs/util/remix/tsconfig.lib.json create mode 100644 libs/util/remix/tsconfig.spec.json create mode 100644 libs/util/remix/vite.config.ts create mode 100644 nx.json create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100755 scripts/typecheck.js create mode 100644 tsconfig.base.json create mode 100644 vitest.workspace.ts diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..f25034b5 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +node_modules +.wrangler \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..e41eaa81 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,41 @@ +{ + "root": true, + "ignorePatterns": ["**/*"], + "plugins": ["@nx"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": { + "@nx/enforce-module-boundaries": [ + "error", + { + "enforceBuildableLibDependency": true, + "allow": [], + "depConstraints": [ + { + "sourceTag": "*", + "onlyDependOnLibsWithTags": ["*"] + } + ] + } + ] + } + }, + { + "files": ["*.ts", "*.tsx"], + "extends": ["plugin:@nx/typescript"], + "rules": { + "@typescript-eslint/no-extra-semi": "error", + "no-extra-semi": "off" + } + }, + { + "files": ["*.js", "*.jsx"], + "extends": ["plugin:@nx/javascript"], + "rules": { + "@typescript-eslint/no-extra-semi": "error", + "no-extra-semi": "off" + } + } + ] +} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 00000000..c882f027 --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,67 @@ +on: + pull_request: + branches: [main] +jobs: + build: + runs-on: ubuntu-latest + name: Build + steps: + - uses: pnpm/action-setup@v4 + with: + version: 9 + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - run: pnpm install --frozen-lockfile + - run: pnpm nx affected --targets=build --base=origin/main --head=HEAD + test: + runs-on: ubuntu-latest + name: Unit tests + steps: + - uses: pnpm/action-setup@v4 + with: + version: 9 + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - run: pnpm install --frozen-lockfile + - run: pnpm nx affected --targets=test --base=origin/main --head=HEAD --passWithNoTests --watch=false + static-analysis: + runs-on: ubuntu-latest + name: Static Analyzis + steps: + - uses: pnpm/action-setup@v4 + with: + version: 9 + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - run: pnpm install --frozen-lockfile + - run: pnpm nx affected --targets=lint --base=origin/main --head=HEAD + - run: NX_BASE=origin/main NX_HEAD=HEAD ./scripts/typecheck.js + - run: NX_BASE=origin/main NX_HEAD=HEAD pnpm nx format:check + e2e: + runs-on: ubuntu-latest + permissions: + contents: read + deployments: write + name: E2E tests + env: + VERSION: ${{ github.sha }} + steps: + - uses: pnpm/action-setup@v4 + with: + version: 9 + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - run: pnpm install --frozen-lockfile + - run: npx playwright install --with-deps + - name: E2E + run: pnpm nx affected --targets=e2e --base=origin/main --head=HEAD + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: dist/.playwright/ + retention-days: 3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..ca083c8b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,26 @@ +on: + push: + tags: + - 'v**' +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write + name: Release + steps: + - uses: pnpm/action-setup@v4 + with: + version: 9 + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - run: pnpm install --frozen-lockfile + - run: pnpm nx build web-ui --skip-nx-cache + - run: cd dist/apps/web-ui && zip -r ../../../ui-${{ github.ref_name }}.zip . + - name: Create a release + uses: ncipollo/release-action@v1 + with: + artifacts: ui-${{ github.ref_name }}.zip + generateReleaseNotes: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..04646382 --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# compiled output +dist +tmp +/out-tsc + +# dependencies +node_modules + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# misc +/.sass-cache +/connect.lock +/coverage +/libpeerconnection.log +npm-debug.log +yarn-error.log +testem.log +/typings + +# System Files +.DS_Store +Thumbs.db + +.nx/cache +.nx/workspace-data +/.editorconfig +/.vscode +/db.json +.wrangler +.nx +**/playwright/.auth +test-results diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..e9b1361e --- /dev/null +++ b/.npmrc @@ -0,0 +1,4 @@ +strict-peer-dependencies=false +auto-install-peers=true +enable-modules-dir=true +ignore-dep-scripts=true diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..e26f0b3f --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +# Add files here to ignore them from prettier formatting +/dist +/coverage +/.nx/cache +/.nx/workspace-data \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..544138be --- /dev/null +++ b/.prettierrc @@ -0,0 +1,3 @@ +{ + "singleQuote": true +} diff --git a/@types/global-env.d.ts b/@types/global-env.d.ts new file mode 100644 index 00000000..81c972c5 --- /dev/null +++ b/@types/global-env.d.ts @@ -0,0 +1,8 @@ +interface Env { + VERSION: string; + [key: string]: string & {}; +} + +declare module globalThis { + var env: Env; +} diff --git a/@types/vite.d.ts b/@types/vite.d.ts new file mode 100644 index 00000000..302db698 --- /dev/null +++ b/@types/vite.d.ts @@ -0,0 +1,4 @@ +declare module '*?url' { + const content: string; + export default content; +} diff --git a/README.md b/README.md new file mode 100644 index 00000000..5bc4716e --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# Restate Web UI + +This repository is for Restate Web UI. + +## Getting started + +- This repository uses [`pnpm`](https://pnpm.io) as the package manager. If you are not contributing to the project, you do not need to install `pnpm`. + +```sh +# Install dependencies +pnpm install +``` + +- This repository utilizes [`nx`](https://nx.dev) for the monorepo structure. Each package within the monorepo has multiple targets. To run a target for a package, use commands like: + +```sh +# pnpm nx <...options> + +# Run the web ui app in dev mode with mock configuration +pnpm nx serve web-ui --configuration=mock + +# Run the ui-button unit tests in watch mode +pnpm nx run test ui-button --watch +``` + +Details of each package's targets can be available in the `project.json` file within each package. + +## Important Packages + +- [Web UI App](apps/web-ui/README.md) +- [Admin Api Client](libs/data-access/admin-api/README.md) diff --git a/apps/mock-admin-api/.eslintrc.json b/apps/mock-admin-api/.eslintrc.json new file mode 100644 index 00000000..9d9c0db5 --- /dev/null +++ b/apps/mock-admin-api/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/apps/mock-admin-api/project.json b/apps/mock-admin-api/project.json new file mode 100644 index 00000000..63c14f41 --- /dev/null +++ b/apps/mock-admin-api/project.json @@ -0,0 +1,65 @@ +{ + "name": "mock-admin-api", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "apps/mock-admin-api/src", + "projectType": "application", + "tags": [], + "targets": { + "build": { + "executor": "@nx/esbuild:esbuild", + "outputs": ["{options.outputPath}"], + "defaultConfiguration": "production", + "options": { + "platform": "node", + "outputPath": "dist/apps/mock-admin-api", + "format": ["cjs"], + "bundle": true, + "main": "apps/mock-admin-api/src/main.ts", + "tsConfig": "apps/mock-admin-api/tsconfig.app.json", + "assets": ["apps/mock-admin-api/src/assets"], + "generatePackageJson": true, + "esbuildOptions": { + "sourcemap": true, + "outExtension": { + ".js": ".js" + } + } + }, + "configurations": { + "proxy": { + "main": "apps/mock-admin-api/src/proxy.ts" + }, + "development": {}, + "production": { + "esbuildOptions": { + "sourcemap": false, + "outExtension": { + ".js": ".js" + } + } + } + } + }, + "serve": { + "executor": "@nx/js:node", + "defaultConfiguration": "development", + "dependsOn": ["build"], + "options": { + "port": 0, + "buildTarget": "mock-admin-api:build", + "runBuildTargetDependencies": false + }, + "configurations": { + "development": { + "buildTarget": "mock-admin-api:build:development" + }, + "proxy": { + "buildTarget": "mock-admin-api:build:proxy" + }, + "production": { + "buildTarget": "mock-admin-api:build:production" + } + } + } + } +} diff --git a/apps/mock-admin-api/src/main.ts b/apps/mock-admin-api/src/main.ts new file mode 100644 index 00000000..9b798178 --- /dev/null +++ b/apps/mock-admin-api/src/main.ts @@ -0,0 +1,15 @@ +import { adminApiMockHandlers } from '@restate/data-access/admin-api-fixtures'; + +const port = process.env.PORT ? Number(process.env.PORT) : 4001; + +import { createMiddleware } from '@mswjs/http-middleware'; +import express from 'express'; +import cors from 'cors'; + +const app = express(); + +app.use(cors()); +app.use(express.json()); +app.use(createMiddleware(...adminApiMockHandlers)); + +app.listen(port); diff --git a/apps/mock-admin-api/src/proxy.ts b/apps/mock-admin-api/src/proxy.ts new file mode 100644 index 00000000..b81b1fc0 --- /dev/null +++ b/apps/mock-admin-api/src/proxy.ts @@ -0,0 +1,38 @@ +const port = process.env.PORT ? Number(process.env.PORT) : 4001; +const ADMIN_ENDPOINT = process.env.ADMIN_ENDPOINT ?? 'http://localhost:9070'; +import express, { RequestHandler } from 'express'; +import cors from 'cors'; + +const app = express(); + +app.use(cors()); +app.use(express.json()); + +const proxyHandler: RequestHandler = async (req, res) => { + const response = await fetch(`${ADMIN_ENDPOINT}${req.url}`, { + method: req.method, + headers: new Headers(req.headers as Record), + ...(req.body && + ['POST', 'PUT'].includes(req.method) && { + body: JSON.stringify(req.body), + }), + }); + + response.body?.pipeTo( + new WritableStream({ + start() { + res.statusCode = response.status; + response.headers.forEach((v, n) => res.setHeader(n, v)); + }, + write(chunk) { + res.write(chunk); + }, + close() { + res.end(); + }, + }) + ); +}; + +app.all('*', proxyHandler); +app.listen(port); diff --git a/apps/mock-admin-api/tsconfig.app.json b/apps/mock-admin-api/tsconfig.app.json new file mode 100644 index 00000000..f5e2e085 --- /dev/null +++ b/apps/mock-admin-api/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["node"] + }, + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"], + "include": ["src/**/*.ts"] +} diff --git a/apps/mock-admin-api/tsconfig.json b/apps/mock-admin-api/tsconfig.json new file mode 100644 index 00000000..baefbabd --- /dev/null +++ b/apps/mock-admin-api/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.app.json" + } + ], + "compilerOptions": { + "esModuleInterop": true, + "resolveJsonModule": true + } +} diff --git a/apps/web-ui-e2e/.eslintrc.json b/apps/web-ui-e2e/.eslintrc.json new file mode 100644 index 00000000..fbf2c975 --- /dev/null +++ b/apps/web-ui-e2e/.eslintrc.json @@ -0,0 +1,22 @@ +{ + "extends": ["plugin:playwright/recommended", "../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["src/**/*.{ts,js,tsx,jsx}"], + "rules": {} + } + ] +} diff --git a/apps/web-ui-e2e/playwright.config.ts b/apps/web-ui-e2e/playwright.config.ts new file mode 100644 index 00000000..750f25e8 --- /dev/null +++ b/apps/web-ui-e2e/playwright.config.ts @@ -0,0 +1,70 @@ +import { defineConfig, devices } from '@playwright/test'; +import { nxE2EPreset } from '@nx/playwright/preset'; + +import { workspaceRoot } from '@nx/devkit'; + +// For CI, you may want to set BASE_URL to the deployed application. +const baseURL = process.env['BASE_URL'] || 'http://localhost:4300'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + ...nxE2EPreset(__filename, { testDir: './src' }), + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + baseURL, + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'retain-on-failure', + video: 'retain-on-failure', + }, + /* Run your local dev server before starting the tests */ + webServer: { + command: 'SCENARIO=E2E pnpm exec nx serve web-ui -c mock', + reuseExistingServer: !process.env.CI, + cwd: workspaceRoot, + url: 'http://localhost:4001/version', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + + // Uncomment for mobile browsers support + /* { + name: 'Mobile Chrome', + use: { ...devices['Pixel 5'] }, + }, + { + name: 'Mobile Safari', + use: { ...devices['iPhone 12'] }, + }, */ + + // Uncomment for branded browsers + /* { + name: 'Microsoft Edge', + use: { ...devices['Desktop Edge'], channel: 'msedge' }, + }, + { + name: 'Google Chrome', + use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + } */ + ], +}); diff --git a/apps/web-ui-e2e/project.json b/apps/web-ui-e2e/project.json new file mode 100644 index 00000000..df8740a9 --- /dev/null +++ b/apps/web-ui-e2e/project.json @@ -0,0 +1,10 @@ +{ + "name": "web-ui-e2e", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "projectType": "application", + "sourceRoot": "apps/web-ui-e2e/src", + "tags": [], + "implicitDependencies": ["web-ui"], + "// targets": "to see all targets run: nx show project web-ui-e2e --web", + "targets": {} +} diff --git a/apps/web-ui-e2e/src/example.spec.ts b/apps/web-ui-e2e/src/example.spec.ts new file mode 100644 index 00000000..bf820c2a --- /dev/null +++ b/apps/web-ui-e2e/src/example.spec.ts @@ -0,0 +1,9 @@ +import { test, expect } from '@playwright/test'; + +test('has title', async ({ page }) => { + const listDeployment = page.waitForResponse(`**/deployments`); + await page.goto('/'); + await listDeployment; + // Expect h3 to contain a substring. + expect(await page.locator('h3').innerText()).toContain('No deployment'); +}); diff --git a/apps/web-ui-e2e/tsconfig.json b/apps/web-ui-e2e/tsconfig.json new file mode 100644 index 00000000..114364a1 --- /dev/null +++ b/apps/web-ui-e2e/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "allowJs": true, + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "sourceMap": false + }, + "include": [ + "**/*.ts", + "**/*.js", + "playwright.config.ts", + "src/**/*.spec.ts", + "src/**/*.spec.js", + "src/**/*.test.ts", + "src/**/*.test.js", + "src/**/*.d.ts" + ] +} diff --git a/apps/web-ui/.eslintignore b/apps/web-ui/.eslintignore new file mode 100644 index 00000000..bce5a5fb --- /dev/null +++ b/apps/web-ui/.eslintignore @@ -0,0 +1,2 @@ +build +public/build \ No newline at end of file diff --git a/apps/web-ui/.eslintrc.json b/apps/web-ui/.eslintrc.json new file mode 100644 index 00000000..9d9c0db5 --- /dev/null +++ b/apps/web-ui/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/apps/web-ui/.gitignore b/apps/web-ui/.gitignore new file mode 100644 index 00000000..9ca4842f --- /dev/null +++ b/apps/web-ui/.gitignore @@ -0,0 +1,4 @@ +.cache +build +public/build +.env diff --git a/apps/web-ui/README.md b/apps/web-ui/README.md new file mode 100644 index 00000000..14bd18dd --- /dev/null +++ b/apps/web-ui/README.md @@ -0,0 +1,22 @@ +# Web UI App + +This application is for Restate Web UI, developed using [`Remix`](https://remix.run/), and served as a [SPA](https://remix.run/docs/en/main/guides/spa-mode). + +### Commands + +```sh +# Run Web UI app in dev mode +pnpm nx serve web-ui -c mock + +# Build Web UI app in prod mode +pnpm nx build web-ui + +# Start the Web UI app in prod mode +pnpm nx start web-ui -c mock|dev|prod + +# Run unit tests for Web UI app +pnpm nx test web-ui + +# Run end-to-end tests for Web UI app +pnpm nx e2e web-ui-e2e +``` diff --git a/apps/web-ui/app/entry.client.tsx b/apps/web-ui/app/entry.client.tsx new file mode 100644 index 00000000..afdfc0ce --- /dev/null +++ b/apps/web-ui/app/entry.client.tsx @@ -0,0 +1,12 @@ +import { RemixBrowser } from '@remix-run/react'; +import { startTransition, StrictMode } from 'react'; +import { hydrateRoot } from 'react-dom/client'; + +startTransition(() => { + hydrateRoot( + document, + + + + ); +}); diff --git a/apps/web-ui/app/entry.server.tsx b/apps/web-ui/app/entry.server.tsx new file mode 100644 index 00000000..f65553dc --- /dev/null +++ b/apps/web-ui/app/entry.server.tsx @@ -0,0 +1,19 @@ +import type { EntryContext } from '@remix-run/node'; +import { RemixServer } from '@remix-run/react'; +import { renderToString } from 'react-dom/server'; + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + let html = renderToString( + + ); + html = '\n' + html; + return new Response(html, { + headers: { 'Content-Type': 'text/html' }, + status: responseStatusCode, + }); +} diff --git a/apps/web-ui/app/root.tsx b/apps/web-ui/app/root.tsx new file mode 100644 index 00000000..aa89ec5e --- /dev/null +++ b/apps/web-ui/app/root.tsx @@ -0,0 +1,196 @@ +import { + Links, + Meta, + Outlet, + Path, + Scripts, + ScrollRestoration, + useNavigate, +} from '@remix-run/react'; +import styles from './tailwind.css?url'; +import type { LinksFunction } from '@remix-run/node'; +import { LayoutOutlet, LayoutProvider, LayoutZone } from '@restate/ui/layout'; +import { RouterProvider } from 'react-aria-components'; +import { Button, Spinner } from '@restate/ui/button'; +import { useCallback } from 'react'; +import { QueryProvider } from '@restate/util/react-query'; +import { + AdminBaseURLProvider, + useVersion, +} from '@restate/data-access/admin-api'; +import { Nav, NavItem } from '@restate/ui/nav'; +import { Icon, IconName } from '@restate/ui/icons'; +import { tv } from 'tailwind-variants'; + +export const links: LinksFunction = () => [ + { + rel: 'preconnect', + href: 'https://rsms.me/', + }, + { rel: 'stylesheet', href: styles }, + { rel: 'stylesheet', href: 'https://rsms.me/inter/inter.css' }, + { rel: 'apple-touch-icon', href: '/apple-touch-icon.png', sizes: '180x180' }, + { + rel: 'icon', + type: 'image/png', + href: '/favicon-32x32.png', + sizes: '32x32', + }, + { + rel: 'icon', + type: 'image/png', + href: '/favicon-16x16.png', + sizes: '16x16', + }, + { rel: 'manifest', href: '/site.webmanifest' }, + { rel: 'mask-icon', href: '/safari-pinned-tab.svg', color: '#222452' }, +]; + +export function Layout({ children }: { children: React.ReactNode }) { + const remixNavigate = useNavigate(); + + const navigate = useCallback( + (to: string | Partial) => { + remixNavigate(to, { preventScrollReset: true }); + }, + [remixNavigate] + ); + + return ( + + + + + + + + Restate UI + + + + + + {children} + + + + + + ); +} + +const miniStyles = tv({ + base: '', + slots: { + container: 'relative w-3 h-3 text-xs', + icon: 'absolute left-0 top-[1px] w-3 h-3 stroke-0 fill-current', + animation: + 'absolute inset-left-0 top-[1px] w-3 h-3 stroke-[4px] fill-current opacity-20', + }, + variants: { + status: { + PENDING: { + container: 'text-yellow-500', + animation: 'animate-ping', + }, + DEGRADED: { + container: 'text-yellow-500', + animation: 'animate-ping', + }, + ACTIVE: { container: 'text-green-500', animation: 'animate-ping' }, + HEALTHY: { container: 'text-green-500', animation: 'animate-ping' }, + FAILED: { container: 'text-red-500', animation: 'animate-ping' }, + DELETED: { container: 'text-gray-400', animation: 'hidden' }, + }, + }, +}); +// TODO +function Version() { + const { data } = useVersion(); + + if (!data?.version) { + return null; + } + + return ( + + v{data?.version} + + ); +} + +function getCookieValue(name: string) { + const cookies = document.cookie + .split(';') + .map((cookie) => cookie.trim().split('=')); + const cookieValue = cookies.find(([key]) => key === name)?.at(1); + return cookieValue ? decodeURIComponent(cookieValue) : null; +} + +export default function App() { + const { container, icon, animation } = miniStyles(); + + return ( + + + + + + +
+
+ +
+ + + + +
+
+
+
+ ); +} + +// TODO: implement proper loader +export function HydrateFallback() { + return ( +

+ + Loading... +

+ ); +} diff --git a/apps/web-ui/app/routes/_index.tsx b/apps/web-ui/app/routes/_index.tsx new file mode 100644 index 00000000..19fb55a9 --- /dev/null +++ b/apps/web-ui/app/routes/_index.tsx @@ -0,0 +1,4 @@ +import { redirect } from '@remix-run/react'; + +export const clientLoader = () => redirect('/overview'); +export default () => null; diff --git a/apps/web-ui/app/routes/overview.tsx b/apps/web-ui/app/routes/overview.tsx new file mode 100644 index 00000000..04d83336 --- /dev/null +++ b/apps/web-ui/app/routes/overview.tsx @@ -0,0 +1,3 @@ +import { overview } from '@restate/features/overview-route'; + +export default overview.Component; diff --git a/apps/web-ui/app/tailwind.css b/apps/web-ui/app/tailwind.css new file mode 100644 index 00000000..a6ecf8d0 --- /dev/null +++ b/apps/web-ui/app/tailwind.css @@ -0,0 +1,9 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + html { + font-family: 'Inter var', 'Helvetica', system-ui, sans-serif; + } +} diff --git a/apps/web-ui/package.json b/apps/web-ui/package.json new file mode 100644 index 00000000..55b8032b --- /dev/null +++ b/apps/web-ui/package.json @@ -0,0 +1,27 @@ +{ + "private": true, + "name": "web-ui", + "description": "", + "license": "", + "scripts": {}, + "type": "module", + "dependencies": { + "@remix-run/node": "^2.8.1", + "@remix-run/react": "^2.8.1", + "@remix-run/serve": "^2.8.1", + "isbot": "^4.4.0", + "react": "19.0.0-rc-65903583-20240805", + "react-dom": "19.0.0-rc-65903583-20240805" + }, + "devDependencies": { + "@remix-run/dev": "^2.8.1", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "eslint": "^8.56.0", + "typescript": "~5.5.2" + }, + "engines": { + "node": ">=14" + }, + "sideEffects": false +} diff --git a/apps/web-ui/postcss.config.cjs b/apps/web-ui/postcss.config.cjs new file mode 100644 index 00000000..8d55d169 --- /dev/null +++ b/apps/web-ui/postcss.config.cjs @@ -0,0 +1,15 @@ +const { join } = require('path'); + +// Note: If you use library-specific PostCSS/Tailwind configuration then you should remove the `postcssConfig` build +// option from your application's configuration (i.e. project.json). +// +// See: https://nx.dev/guides/using-tailwind-css-in-react#step-4:-applying-configuration-to-libraries + +module.exports = { + plugins: { + tailwindcss: { + config: join(__dirname, 'tailwind.config.cjs'), + }, + autoprefixer: {}, + }, +}; diff --git a/apps/web-ui/project.json b/apps/web-ui/project.json new file mode 100644 index 00000000..dfeeb452 --- /dev/null +++ b/apps/web-ui/project.json @@ -0,0 +1,63 @@ +{ + "name": "web-ui", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "apps/web-ui", + "projectType": "application", + "tags": [], + "// targets": "to see all targets run: nx show project web-ui --web", + "targets": { + "serve": { + "executor": "nx:run-commands", + "options": { + "commands": [ + { + "command": "cd apps/web-ui && ../../node_modules/.bin/remix vite:dev --port=4300" + } + ] + }, + "configurations": { + "mock": { + "commands": [ + { + "command": "nx serve mock-admin-api" + }, + { + "command": "cd apps/web-ui && ADMIN_BASE_URL=http://localhost:4001 ../../node_modules/.bin/remix vite:dev --port=4300" + } + ], + "parallel": true + }, + "local": { + "commands": [ + { + "command": "nx serve mock-admin-api -c proxy" + }, + { + "command": "cd apps/web-ui && ADMIN_BASE_URL=http://localhost:4001 ../../node_modules/.bin/remix vite:dev --port=4300" + } + ], + "parallel": true + } + } + }, + "build": { + "executor": "nx:run-commands", + "options": { + "commands": [ + "rm -R -f dist/apps/web-ui", + "cd apps/web-ui && ../../node_modules/.bin/remix vite:build", + "mkdir -p dist/apps/web-ui", + "mv apps/web-ui/build/client/* dist/apps/web-ui", + "rm -R apps/web-ui/build" + ], + "parallel": false + } + }, + "start": { + "executor": "nx:run-commands", + "options": { + "command": "cd apps/web-ui && ../../node_modules/.bin/remix vite:build && ../../node_modules/.bin/vite preview --port=4300" + } + } + } +} diff --git a/apps/web-ui/public/android-chrome-192x192.png b/apps/web-ui/public/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..a5790c0e20d2dbada30bf0a7fa6f65959ea06997 GIT binary patch literal 8143 zcmZ{JWmFtZ6Yk))I0Q+64el%u2pTK|HaNjug1aux;_eBqA%Q?}53sleC&4}NCb&B+ za5vxmb${G5XQt2ebe-<$uBYm$su*=u1wwpUd;kClp^C3HQEk`18y6e()LeGNK{c2* zGAc3vP@6=6G{-`{gDn*`RRF+`6#(Gj0C0=yf^P$W7X$!y%>Y0w69A}Ob6YheP#>@@ zloVbAkN>VhL}?nT2hUYe-va==pZ&YhN_%{EQJpxRP!)NcZEQ3`7Tlu=dk_H73qxPa zX!$H2tiS>&yl!!hCj{%CW2Po!epbZeFpC|6vNN))NTg(k1EuJ?#!8KDlOc z$C!ueQ{vsyvN7`{Oz5GqvIHy(6yOGwsJf>2*3CUyH_1WTx{qcaUEZ?#E zv%D;q=eRHNK=F1K5Ox*Hh(KjCzL<3^MU&561Y~#Y`0cp+Sd41uzJl{n03Y^uG$7fkLw_Pmgl#mt@X;ML6C|PWw zI$9w`UAPi{@esh7%FL<$fom&az=h!A)j(o+1z(To>(>&0ydN}!p1u^k6RejaZ^oH? zQuqW|!(R6Wn$c{(!YYJyKwzpaJ3tgO*AkqQDH!t~xs?E&%ioA(VOgFim#^I2ogzs&sRyJ02D@}gW>^C>BpZ~UwDs-wy!Yr-t%%Y98JXTC zPDnh{h8rOEZwR>p&2^7Ts+5{ag5(z%1dhQh?dp2I0?O~{q6VD(2ASp~&?l|2B&0^D zo0TS{J(b|_^%UZ_>ehmj3KJOM?YQn3X6Tuq=A6@%<(;=0ysK-wD)}Rp8ExSp5dl$b zZA9UyvYKhN#Ux@Czw)eHAp0q33@qSIOe%e{cEwWWJ2CcL49XXVB!4whvieXC3_5E zCHVS!N+2b>sf@%RRYlrJUy^l6|D$bOkCO)0r$QnP59)R@c(YSdf;#aF)=8$0pdzaq zUj=Vl$B{mj?_k>@@hDXp^RM#Z1%0eNr4&Dv|H40(BgnG-y3ZVp3S- zerJ%db(--hW4xF9o!t?5M_a)&yog!YWp*YIZJ%Uj0|JR>9W;~a%vx0V;ZsaMwc3zYh-s3a*Q+aE|>RyLK*MTTzLnrkX^%~aUBU9f;f?9oF^CjFDJep^%gxAinN zt0363Q{F+p?zpi$$(`EF6amvEDNi1>ey3;scfiBTxcd_)3C$=&begHFSrGF`9JhYa zxbFWw-UHVJbnUxV-eMW;w?$S|1twj*`K8Z_&N@Y3Eqx+Ld|qHZsmv zNoCaKLIfI{tnAxSpdxe`VYUHkS{KT|+l3X%!j08s?*hkk0Vq8&YS!_Qu`%wXO1ltR zayU)WE&?aij1WDcdgsob94sPI#|9@sz@$kkx}|N+%(R+!JF11zs*)!7a8;9-`}W}OgE`KJRTXAslV;#p|+dTIR!mElQ)YLzl zo>ti!k!sn&?;>lJLmlZJWBtNp;|9)4-H=Si^F(zLM`(!WFHw^ldTD0(`cK@JorD^# z9i#&NuNa+!Eq8 zn?2NjJ4(nf3F+3*2ap#;ss#6UUi}sNo21n0XOi~|^;E~Jcc$jX%M;#7wJA~Je{}00}^Rf3izFE-S{V1_3@klZb2UUv1l~W3PyN=mXD-#PX29`qowg<%o^> zRL_s&W7lS-fD5>xy86yR*F}z5eO6w_?pYVyIr9bon~U2@1EJmw)AmFvQ)&$TrWKbE zX3~7$#nTgPMul^kavcIv8u#( z>plizI2pWbhF1Lk9b!ARtTxk1C*IV1+X91J*2?G{IUr#7xfuX_;XC}w3)qlfI);x-VWNuU*)t}uF+rJv@0d$kvPBpj^1<&XETlPXuiC>=3%-lPAlwps1OswKy*K!c66-HH|~K)t@}3wf>f6Y^wTGW z7`4t6vb(#TAdcICiqIc@W$(UwOTAL9;~Q?U6jpdr%|%y&_9Xqe@PEeT8JTv`nwBeJr+IaEjQ|*~-3g zr7i0y!nx^L4WEm&5YAQ_!v_b6buN@T9Oj;b=*0*PgMJ1vsxtOmyM>)x?A{z-QH2HS zvVP<=scSYJqQmqvIx?I7xreWw(hV-2xQ8)uH%NN#OGIyMkj2gN@$$;Z_IT%xj>{aa z22UYVtmB>pQPZq2++fLzc(ThK_5Ok2BBf%$FWc# zVnhr-rX`vN5+osyV>ZWA8+IGmH`qT6FCAzXDWw}$i@yHB)rH5>3fV{h?mc{Gd^JA8 z_u6QA;`sbrdaF&$HYjYlNX`&Mv-}i?_{-+`&e<&0b|3CM^PMo^4%Q?IB1~gsiVSGc zxq49AO5FAcrrKW&#Kpo&zYv7vP$KJiXny<{ocyM1^TN5p_#Fs1zI2ZNI`k@;8PEUn z8CpHp1y+SP_9K{-nGb>Crky8-WxjbEK`m%qSbh<_(!mfGrP|Ne$!tfT!#$EIVMj4r zk)PD$ly@olRXduQZ(;D|`E&OT(xS@m=(&>qXr_Y?;dJ7}T?liG2FEF_vuoNHuD$RF zV^R`Vaqs>4q@S8;@_VKTgHRQ`CAU9s`TfpuqqvoxRGQ}K@Z>PMzh=ml$O?P02_3sy z>pn=mN6JQqZPO!|G?bjs_vd|q;=$4G*-HN!)Uj6F_z|cUyh`%-_pXC(wu&Df4<`O3aHwB71UPNd+*IuQ{&9jKi>swZB|$y;H>gX3#N8$Z8rp|(ZG zx+k3{7%VCI6yfI;+2)U-vUzrK(KECi#Lgr1+;@$?<@BB(!;Y-$;!v{dBUisrkc)86 z$yBOd{4*iLUnL`y7u)oSE}gy%a*^jcjSnw#U>3)dW!Lvfz1f3*|5|s&?^xFE)_=F1 zk~O|kv5^M5EDZAPi=r>?(#0FtR{`I@V_n-{5I6F|4vpwL67TWy^XacqT9!R{cz75o zLwxbN&6}q-@b1-zEbcuX9W76+y}8HGZ@NlT#R@Hdb-y(^8Gcp#70?CEr`{c*>hZbcmbk&AQN_6 zwobeL=Sl*Q;X*Fb0CsE<{?zdJ+5%)H-#-Wb8 zShu>3gp@RtPMm1no#%@4 zVCnQgq--h4dV{~k1edhD?@dIsaU11me3h|W@BkWecRvEXYN26Q}$}!Z{joTVD(NQY?hi)0V+y7o*v$x*?;*FHZ6)g@N z89pWAu~!Iyvxy3{`T3yJP>l3wXlGV)<0hzp&ws*(J$z_U) z^Ui6fy*=n83|DG1VP*#?;Dtpg_p2E;c80JqwqN&9w+r04}#=uINopdYB0c{NXO%U=-bu^!EaEhVaf3}@-pnIw9w1Ji})au#x&rD=gn&s z1^ZpRu}y!g%T3{@TJx-mS|N@b(`7~y!rr`3cyBe&(m$$X`)thUK1`GgXueYe3;1Do zAdg_#-jR0mHE1RQZt*2|4wiL_z*GJl%78j<`YHbUTz zH>35$O~%Y064Gp8r%!f{EG$;@!Qio7_x5&LJK78$D6p%9qaIb%UtLjoZ40qYUrkFg z>|!R9#v#vt=Ili?8c#sWrm3d~9JYQ7oB5L~nNezIB>Y|gn6&=MS)_xaK|Y%orI78S ze4UJsft<~=Re9;S#Ke6lt$9J?-LF&mzHyKhqmQzS7n^85v3b?Upx)-c&WG#@eaB)2 zN%;3_mC1^mHx{2RF?<)WV=!xkT#g2O4rbU$MP-+22T_i-lDF8r7uzRrb&X%ZAP0sI zxbDxpsW^W==;JPz7WRC+hgo(nP1wDv>q*SRNp9zxDHm95bcp$)Wb(5;Zc{!5X=>3{U5t2BU&O4P~ z0#327(QT^~l3d%*CSru`DD8x~GWk8qc57qb>-S7^B(3k1zOl{E984c3bKbWp2XXl8Glo-xRfYJRE$YA_9NhM9=5{RmX1({SbUoj{p+HxZ>Ffo4 z@CIR2P^SiAT1X$IG`U>V`9Q92ehqXXE31^>Y7s}oN8Q*aEjpScpuvaM;Kf|fhi20t zXwS4A!>9F6Rvusx^wvvyxC*=lTdx?!_)YRGji27{_EkS(RgW20-DW*z@?-EHHR5Fx zr}T{Kby!F~LEM?L$9g0t^~~j?L#?|+VO<&$FTR%kJ#cq-KjZh99V}-Xv|XOZAb2== zMo;fq{rArQcaMC#$VK2-w)$R_GB$Qj-%+PmD~w>Sp&hkTgdQfwk)ffC+Iz6=$tOw~ z?eT@u2ZmOM3m%whrUArTxuxIb2&)iyvxk9xk~0lu!WfQJl*ZkAF36`Rd%+sU12sig z<_}04A%?-UtlHCGnC$tazj^F=gzml3N5YXa7sDt!J?B@JP z=gG17v0>M8=X~IALH8;(CMNC4?`yXMh5Xs4VFWC2d2~Iyvu1I(n^Wz$IdPO*?`1L_ zsQYtLuJ*8s5MwgmWX#D$);csZT2DJ>NZg=Fqf@>$``Y%4%z0>pt1l{UZrazkUcbBZ zRg9e0x$@^PWjR?Iskr0?N!`PbqaoGRO=(C9stk*P(Jz@OorsN7a`M|h?^BNjfjB8( z!0g?`u!`+m6$`!pc@b@fxbNw+5=1+-{}sJ9f(I4V_`s$J%A?*^Ky_wmlye(!6%wr% zZj(g;P60(S%9**Woo!= zS*03JBS=y`=Bs(;rl7(qxT=4Rr4y80xHB)Gn5fS|rWWETtwD`LMD4Z?*GN147b{&g z;{6Y8mXa`D;xrSVxpK~|`qU~(>_4(@SguA74?HI)sXMrMCv)xl|C~zChbu?nn}W@E z_=NbyiXfu@GO^E(Vk3@}WyuL6Im^DbKVND)x}Vs{vN6IFq@)2Q<5Nyij~5Nqg~v<` znr)o%XR9H!)YZew_;_?yhQ>calp?UZjC`B|xVuSxd|E7b6!?nN7!DZR4wnidp=ngV z&+QIMJ>1{C#V8vpWOv?r{#|*KwX}KtV9U|{wA!GN#)a?N=(uwyXKoW`|{CG ziinC})#KCxpPNgpOgaK6|0#KO=M96~>Ds>g+qe5)PMu{%fyEuV6oe7N;1dj2+wZ>= z93dV)o&)Cmf)@2NJXAgsV+(#dEMTZl}6o@OtT+ zWdJnic&MAu*N5MaQO1K-mEouPwF<(A$Zd$(3d1TkP}DB`NsGN z;@myT&e;lLVBnjvLd7E8eb6}9p{x?lnK;>Coe=8JRVr0!{8^)!{J-3Ct_^;9Zj@H~ zLjw9nHvX&T+92<`zyuGtAvS9pxpZSsrUeQ9QD}r*+3cd1A&<$Rqp;=gPkblmf&{Kp zc!PhZ2#v+Js#$;(CAO{y3_EHuBuiBA-S?ZNwVJ!D4`O@;Ifn4sO_M>kk&QiVE>)gn z&F5&t@8^^O6h?j~&6!Vc>n4BndUNDz-#_XTwDC!7Y@_4uiURcXAypoa2^qs_f|l)j zYuyWtf>s}9J)nX@pYz%|RFdP-qugBL^o)R1(w+6y9WsaWNE;tN_IMVl#_zqnH}qp{ zPTa^w+xo?@4IKZV5F_l9fQ)T5?0x#J?MZ{-8>yw19o=?em}qQl)izzec0jhzC&Md9 z!<7qG+7D3i3A6WsDyfTSrOha0zzg6UB~=c$1Z^q7MXPx+xZ;EE+crkC&h(H|b0`K> z1=GuBZ}GvCdf4_!a?E2`*}nH+Ld(2ikA5hJ*`~MpoRG10Ugxixjz8vuG zEaPJiyWFPB1JBQ^sD!w@1!Cq;7V!u|o1E>5J z=B(_%e~-$WlY!}@@Ofj2zLYq(+9_umVq;pcUXW|yg<@E4^bHrRqym`os5DqAZu3LS zS}IV2y@3yIVNTau)Vb+3BwN2uX%PfJn-n*YnVdH%<`-^0jnXEA>v(3O+lAj<$BueDhbArOY? zHeCl;FHG^aqU6Xy8F&eOcqW~-D*hao^J6v$X{ACioe7kc)s_MQl~*RXp|r}#nG+^A zAtF4zy?q}t@W3y;{=Y#xblY3bm-QHK1Ugq*MvQUH)Y3M3CVU?=;P+sbRS zg*l|0utsdwxa!Y_D`dXG7eC zezgGYyt$zP94=l_%iha&y?LDVoiH>^Y{Ub=vlW@`gQ3@K2;x|EHAqvY%4#t=IeBFV zPGdQ~R#l-{L0Oq;7j2{=E*yeLN?LEFZYZjFP4BzW_ZZvAu9k)gz}qE?MWv3$MWmz} z&`FAxNXd+-HU%G>d@YHe%^Q_#r`Vf>&lp&nxt@dIYoU>NtkVvxjM;hlP0mHLSWAp( z0DwmK>5vWUn~bd$suI%DVx)LJbisO){cD}I#XifnZ_2_?@bw{T^gvKymShu~i1;wu!07q87nj*4N87%p0^g9h-+U=FN}E|j34R!! zN2cZ~hEHCd`kYuv&Ghb@q_FK3IX|khlpFFLlvsP!goQIO&q%7${8=#WiRT>)5q|9c z#OrEcyIL0%iUEFzp~yC!d)1!pVsdm(yiL78qdY-s`3xTUuTmt>5hBc<>Z@VBd%Jc4 zdVPh-K6jaD-rSb;rxufTLi+3K39fL8?CXLYly*BYH7&&TGE2*cglMcN_K09JK4;x= z^r-q2>3K$u_OR$C+61=+zd5rXVMwD^cuPC1Q1{Nl*xraeLkYgyKO`gb zrCCtvfg5_zeau!!NN#|(oC{9}3;^5Du+PHY5h3-MxmN1DA!>rjD(jo-u12ZsBeowV zP$_5xMq`xU2f&M*o_v_I6_p}nT zbhkn^fB;0`1rJ1+2g0w#FDNGXQVb%(1%Ze`AOv|+bN`QklgoQMYv2FBfM1uL14@AA z-wf|utUY1o?p6Q{2IIAJcJQz?ceUbmakt4il%Pco()H8Q^VBqlfnD8QtnD1Fz@EOY zR$x0gy_5v(WzoKNmor literal 0 HcmV?d00001 diff --git a/apps/web-ui/public/android-chrome-512x512.png b/apps/web-ui/public/android-chrome-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..9c328c7ef46be0a1c9ab904d48f22b21ad7ae7ba GIT binary patch literal 22739 zcmX_o1yoes_x2sSTS1xu2}MGBq(MPC1*Aa#N0xO4Bh=j^lN+0Wj`a1AvDVggzM2!eeB8c%o%A`MdII%v0i)!h*-Yu_|c?G}M)}ZAvt)jY zA1MD0ju(QY34T+MeYG~>*-&gb-XW6BW)CjZbbe|;jWlVEAd*f)ooaoSCRj3!$L556 z`ySs!hI{>zO1oA#)6Nm~5}0 zYaUfW9@U!=O|KuLqdm%x4UBD;cSK@2$WfSNMFS-UQr8nzFR@bcZ<(c7F}qXx4q3EV0D}W5}!iNT&XekVkJyf#K zTID}*$>)<Wsw_!Md)*@!`edodp8#H50WsH5Gx|3$X?)4D&E|3w#CXQa z^-!DTIV%*IOBRTM7aR{GgOgd)I+zaEeb1wUbqB{AfN?OAF}jiQU3p|(!7-sQXQB{M zl9K1%=gw7FlmpM!zlI4-jzuiyiZM3c-R9nveneme2}WUB>aT^KCDigV3SNM z|FO*jKfz5!wJ2p=X59kTlMW2zolo|YfmyeIHBxC+#z_a0g$2iNjp?fA$_pde@HZ*Q z!exKtQlwsu&g71{4YzhnltY4EAul%+7AXyFTQVJU0h}rR1Bk3XU6NOhZN_ z4mNU2k)N5D*kN|@A=R*fk%GJqt74LeBj&KV`L?!g8?HAV49xYI*|^LPkdy~uYZmNS z)p8aLSR<3cSK2Nvn=->$-QHm2q-y(fAx>exvYXPLkO=H^#JY<7QaW1tjNS0=JryVbwqQuPvOae+d1JK1 zKsy^jrOQMJiBK^z4h={@RwvHRz&Z=GJe7aeWF&pT$@XNe{`ht%;S?#jhn$vn_-4vY zVwN11!5bG%tng8Dj8kL0Z|%PqwY4p^b#SxaR**ZJx7yleTX;6fO|N zG#nll#%&?Jpun|c_{s1YyOU5LEjPq{^X5%To;Um|5UFBj^18UZ{96VpS^rGJt)``= zTESuSMtnl(W!OM9rla`zkiQ^g?9L2_Q(MX|;BtLyJdCeRn45p7)`ig$K2fz{T9Er~ zm4~ZG6KPY1o>i(MZ0W)6hYuaMCQqK{uk)q9#YR$AqIIe4kHAxd~%o~}1B(NN*PMTT-xv{-15nyRS3Rn=nX)Gmjd zkh{Bq3`E_JF={F4XF1?5H!(3PQhpcY=YHGPVd$K$fXd}IJos5sY{j|Q7%}Ml;qc-l z47gg#ioldn;h8?9^Y3e)aQ<5uCm7{LK-^1tO3>d}!EXnD{_OechB}LIL#Rx&nf`s# zDGI4Wg;`)_6v;`B6Bn(vbbomqV`$1t6&1ekJ`Bw&Nu?lN{z1(9U)h=%-gyj--LzzCU5Byx;VVv z8Y555K}5W^KH-_4A52VbfkXB>)+}&%VvHSu>u&nA^0gMjH>H$y=m`NnlUEsj2_{_t z9Db%?sQQ9Vt{s;|8+>cRkDF=TpDx@xvV0pRMu?0?tLu`B9kh*C$TZH)y);oq$~3dI zXNd26K_-p%^zliO5wJro^h~CZ@lwh|VS9%RSQ7OR9x2mUYp@Y^X2Lo*7~>J!F-^*5 zlc%R__Bvmh@Cz&k&}|x|Vzt1FY8O3i6<0ui zkMzQ3l&lE^MtM89`4Mbs8~sB;Jn>QlE>H|QX9F`cd1u(LT?*k5M$lrgnBXR! zk(0*~l3|G8VpXa4{!pAkA(Rtbib*tXpvCu+%oKB18@(NC;t%<%W^8Shzv?mMexAw) zI}&Z{*k**=Ej?@y78a%yOFUF7e9wMdcv$96`#l0=d3oU@t+3hFAiuD%?y9`W%7wde zae8SQ47qTg02+-%p6IhAjEL6GjIb~xQ^oMZ$7{cqH4~+xJC_NOJ)3c^Sn}$_VqoAq z53?1lj>G?;EOox+>A`~EJe(#CO#HW2RJH@yh7)TmHist+x!gPtaGAx$&s-?SpuSTM zIQ%4@mw51=ghb!%MBMr%GU-qf{wh^2seg*Rx;&HW&Qa;?QF?1)RUVEFA{U`qwlifvl zSogtSxul)?HVx!H3o>T6gf!ap@W|%3hcOb1s#uoNbWWmAUO% zeHOAcaa5*lH|Bw9Q?d{U0-8||Kk;9CEv+XRpZD`WoIt?fr<+zjh=?(FzDf@wx@jXQoy3H+R)w11X&#uuLL5)A1U!Rw8Z0r37AY3V3roX^Hb zEhspIq>(|k(sjK+7K}5`Ms{n$V=l6nP>uV8+%B=12F9oU)ss3>+ zP-gGYP$W(0Hfm!4!)zxobB4mkYRek^-o2;6ZxW_9y9p7M&;T;D`U6& zVd>K+0a8%vas~kq>gwHJnKg=!!zH%EBE(8?rN1OhVgHkA-@?nxXE8ZEE43^T%eL0O zGy0AAFz1@Z(q(9V9rjEx#h&gJW+fljh zj?Q__e!6@2J(L9NJ0);mvdk(CaN|yN1Nkc zo^tFxDTca#AOX948XZd$TmSkTv=}2*rlapVz|9+M*xq`Z8<6VkGF~x%+u2`N8Pp;hQh2ar4 zzz<80$y@w?nS0B5-Ha zN)H9`u^2#L9d$6x@4^jQ*ho=XMhLB>ByVfNZ7)!@pOW;A`$)yV+q|K~# zgbAjpIc$qTzU<%p`lLqt68i6+lwn>7%NGJto{BHe!IcK9L@%gd{RU(|*(r!CZ#?`5 zyrS^wYk<9I6OMZC6C&SJ7&t8dTW9a@AB;GhU!a&2kj8^8iL~YPy|@>B z!~0{4lkH1BB5ilOvV2<%bMt#nk3%&o?Z#;$X|ZLcGYbo8k_`%CQ#{DR9}LBQ@KfL zcb*c()b-MB!vjA%$7i9_#F(5+qo8e+N%9rOtpCXai^NyaA0G_qc5!6iPmst^k3a4p zVX%0yN=;kv<4kg}+1PXXIgL+%`CPhE*$VYp z*3|Fld`aQ(r+VkK8zOccX<`|c2d3XqOrb#6Z!X;!6?3zBIMdv$`2v%P7Eyjz@NdKV z@1?WS<|~(X41UxIH$3+V2@21x)=FEZ`BisA9jVeTo8Nq7ykAcP*4*XvLXFgw!uut! zafb2(-c3!t8jeorN!O{AxUC>SdXOqscD4> z+}4|Dh4EUDu+bdt0;h}5&UE|x1Ugb*4I8pwvPoWVdRImhIgbgwAzS;+Pw#i`*7f0D zgY#1NJ4FM>*#1*vu8^F{Ce?&g&%hxn6>jx#>B;0Uq1{loWP3t0HE{2tm)_vFZUyB ztXPHpqodZe@f_!CznbDG3>K^q$pPG-iGAfS_ftghNgUqRxjt&|>ax1-xbp`E20Kc0 z9Vl}o9gbFC8fH^+98%7}B%=~DfchB5Zr7N9`QvqP#Nl+%tA~nm2|PR6U_qcb?hB|} zo*(NBI~#o{-8kL7d~7qA9eYV#440B(n(~+&w)vGS>HqgR-CJd%VJ6&Ik^A=rD5x*a zBk~R*l|iflS@)+d#3}52uAPcT0hi0DoOqO#s#GK2k(8I;JQmjP;!lVuW!hsi(}L1p zh3{H}9NIoV7f4uT-q-r1@dMQy;R8)n+mH3cyELv>F410{%s${h43q?To0Gc)J3Gfg z+GSsIbJ|nhojy^sAWG^c4^1?!&?{X988b5mqPKPAK$0i3p05uu!?`U93rAOuN=kC4 z4w}6*Ns;C>t*E&7FKhST9@7Z9#H`aKjFg*0Gv2=N&Uatw>PsgBWJsNMBx=&3uxDw?Mct<`=u~L1VQhJbjr=VNi$T`AzcfEC(R3J%S+5W2x)LFapl(8b zxJOqUl@m{f5rjLPi+EJz)ZYjBZix7*n{zNjek znu)4Qe%}$!o-}@nsu#mL3|%e~^`1`{vSelPraX7H6vI+OJho2NNn^j*WK&2$?|mV! zDlqH$H>=k?)$DZ?Ds;M(+jJBPE354dSy&usx0{nIx!^D+rQ(?AwjfJ|)JPPUg#tkDh;%ipJ{Arz5L;U_zO{b^rTtK6~u8XRz zmcE%HmS~tO)-B7nRv(@6eci2^UEObkWSttH8HQ$1wp z0%TyBwy&{~)b&Rn9Ci;5ww*^@1qP(NW?icbr>kC;t>ck+Zrq#R9bT`i-JVd^RbX*> zpp~TBcYPgj1R?)DH&U#l;B*VRSd#9D>CQq?}T&F`4Xxf3O_2Df0IoV32@HIY?R6gcNCYze0{ zIDNh-40au5*~Q0gion#=lt9njFFOqb83#4>-)5MyZqUEMQ@5mo>AKotg38hP*~L`$ zgrW7%i(yoF@{q#ZuviW-_~vy{`BLS#E(B033*<<7>sATk_0NNovOwE1@epsQETev%#Z8{(#*gcq@};Q5$H@8lnsZp8-tx~_!m#4utQDq{$<9TqhR6Imq8pJ5Rm(JKdRn{r zs~QX`J0ysX=B3*s?@j08E+>HSI%?`!7-e2mYRp>kV%ctL@ES3!8D_*|XknCA)_V%f=*Xn-T( z0;p&<=DiC9)H@_2!U#pLdoSl286nu0 ztaW|)Z8ac8dVV~nuh-nPA_Q6t1xCeB9HnQkEGuk>c)(&tKTY1&zg(s-lf;f|Ot5p? z)&pXctvzmjBJZ%2@)BXO{MMkhLQij4^N0KHob!}>|FfRd>o;BLbQ&Mf!Csq+0~~q;I>B%gDIzt$%C`%f-+9t4qPB4}a@$YssCV^S*w{Sg3}6-Q~8ng&byMnfb@Mh5^`1MyJjU>H^LF8-6_?k{-pt|edwRYZw7ijk z#Bq_`;TIRL`2HlD|BK;RzcqpQ@bO>rnYDOrP9>Kj-Y(ah4&mR`>ya&^GxEK{(X2`n z!Ij2Ec7EJ0UN$rmf9%flnb8+2p}sjnkD22O7xQL1-hPmcP)i0H#maM~aC*8{d+`|$ zZ(hQ9QRwAa7BE_yDNa1JdAz5Pb7zAqR16pU4p)|xG%E>0g>i{LK_YVV#Q-z3({xV6 zu1>r1YkKqH!;U1$MM`l|yE{<~{U9Q0t*$t-eN%P)`ZxtATS4*$G$a!3~G;U3C_Y~Sct0UMd<11z$E z-S-}{KX7$J;Tz6)(oLO_K~BJ5b@w15wly12Cc{aK!r7=>ORVk)AvTOn)XPLrXjY9Q)dlW*Fo+7^4?H78p3(R3=VX4Xai_aEyt)b%MnO0*uam6;V}Gzc$%g_`hZ%oh1GV^MvBfqs zjVzMahu&x}Q(rnBAj84M$=PwN&PmCLZ1r=jpZ&K+-PS2{NWFd{tQfJeTb~CzFGZK zgxi+(Mz!5^@pO*IO9fJrp>!ZH5BJDb0u}VXI4!f0OM<9tU|HYcQ1BEWNaop^0Dfd1-2;ITW^PxtNLH~_Fq|3+?r2v{FOcGXg+3z3yJ z9N-OlD=85@vtD)L5Jo$=&i&^C&i2uLz=dwKv|83~q(Amr?8QpTM{73PbFaLMO%k(Y z0|BxKK|&5mEB(fRU=YjvU=aqMF2mcB#QFzn?!g4lpRdjx^f$NanL2(eHTJzqe?F5b z0|9Ir+z>ML8iWwtE}ch8;hr&(@=oge=);MM$(5I$X|=8Cuu) z%NEM+HGo+wsVRp$azKE~Kf^QiW5u|pTBa?^vVZ&>}x#QU9}lyfYPB@ z?|$*~y~D_KaOc+$X;G9`DH4fDI^Mr_8d3#$heaI-Nv^B+I#V5QxA3V9J9qmssDRX3WREK(s z`T2Vl{_Rh|T#BP~^6e+J(5ZUX%S6SpjXK-GZ1U_J$Cj^63Ni0}q}}QFC7)V7S|Eg@ z+cOYlF&D!^YEv^X@N;{SP#Cu}5Cc6^qzXX!Y}TU=5oq z66u zW-VqTKAr}MO8`VfwoNG~l_N{8tPR>nQ8dB*lHQEHsUkWcSV)zQ2TS^OnN6SF19r#5 zT`9I;VcOYFdUW!`gzU3Pg}V8@-pb@DtT3ZTPdrGDw_G?k+`6KyE!Kjx_kL7H8~H!Z$@zOaw);Ddw6+6Bed+4eTfY~!_2%Wc zNTWxOI=KprL^wE@7TJ5c!eYNPZf~Bsq7fPZiwg;fOhL^7#KX=%*z&$j?r^uaBo&~C z6EK?05%_8(M%U!9hC&c{^J$B`u+7g0-6e^dMoUu$8vr}B*w;k2sY_iJooOb7*L|UW z;wOfiXgs3k9`CX>78~;QvTLC|`l}8w9(4FWHdHqU_G%)+@wy}{RXCu@->l)7Y=QCL zW~E|z?LQNiyxe?QFvH4Q2L044X&2NIfja%gJ3?1qSQm@9|02|QhVj+bKxmK_XeRPGoHx#)lYzAw> zXiu>`*z33R#rkqL5a2cB>()c@2~Ng za2^S{nPgf)ycXXb$)N9XA$5o8_#x(I+l?KyJAqs#V#x;|3`^S*?^?~HiV=lTvfHhC zl=K+PsDY7BTqX^78Fu@VTsIdI$>^ta+|yj_r)nAe<TJXRp!NFh#H^T3yP&Z#hE9 z1^+8;uo+BR2Ok^zp_l)a|I0E&f4l zhG`)pDzERxOH*#2K`@M#g0vF_v!?DR_ac{A+WgVP&`oB z{%N*j2IE-$J?v1@3=M_?m0QULWYpKJFGfql`#w&14P(ienlep|72gGX)te)_>g@6u zN~}LbFuUPkGg3&?{lYI@`Zw0*SZqEbiUPR7Z>t%S(VDEd4TpEa+u9Zc+C|#)eU^87 zw`z&^&Z&vLx30AV+7om1&|(PA_p8_^(S*K=O(O>X?@TsyDxYGy%`OF-yP9{Y%dpVkXe zS5ugMhWIUdaYoqEOB!Nge`!Sg89U#7j6i3-U7B!^_$Y>DM0JOmfjCzMaKafWP4sPN zDRidzKZZ-icbOmXry2~QCG0Ausz_HTEDdeFYtx>B{+I5dU@~Z{d^L6Vv_A*XQKKKd zGZOWX9O*`3T_OF_PduX5ML#sSn(=ZQA~4OfdI5mBn=DQg)?GxxfC%*6_8qy~l^Q7U zbAbI_Gxbih2!13j4hke%Ai9P5120}P>xqUbpcHVr5Ro{L^FS}ym$>yYQeMr|q0!r{ zM_W<#0Z>nh|3S9F;f};C280{N$&5(HT|GBIc}AYhd<>J}7ePhI-i$$(v9rC>!DZRU zdEk+oH9LNQl^YKLWvO{P^y>J#Ywh*b3>xuGWyuENNJaYbyu!#90AysM$ptSCX+40D zF85)lYoKQ%2Zn|A#!ej4FK>l}XN)*>j@Wap`Kxl2rM}9urS&lh5W~P+H{0qDC<3ct zRS<|NKjc7HC+sIH0ff9Si+8v=j%{m;Tj6`+4{(?FXCXx=1w44fCxZhHpA&|~1db6{ z%jL;Q=a)Dpm%sMj>K}h}bZdUuUD77}m%&6mcQbC^G>Bf@|2bI{CEJ%{OXf6v`5;_; z?0ejJK!*ORYUEF`lWE@v?2-~X?(dYsjrTEqPHsi^rp5tdeAkvg+_1V^pc5JF+qZG7`u`FIg{pj;X_^r@p8^~uAV9Ei>eclQ>W>Tu8v1Eo zUAHp<2xHN+)-bhx5_;ziP)DQQmFD+@J5kR1m1~B;fe%&MVu~LvT>{qnF>qtzXBmn7 z4sEZNR;f`@i#kW^!<}RNo<&hIBy3Qt7$ncbDI&r|$k zVe_qUMVsCi;ap)44z1E)2%?uqdxw{XRX&&w4Sw-@X{NlvE%DY&9zXG|A3yeWR`~4| zJ+t(9R7hO3vTU7ZNfd2&>rqVoZF13#!QpW;yQifq$~^e(sv^ckfPMxDcJXh~j`^4z z@7(Dg_po!CMGJC6jJm}Y6qa3ZwUoG@9^TugKcspecTxl<-_E#MfOu^@a&lFK#oDD5Aa29pp#tp|7(uY`$f1kYWDt*`tiZlp!0zHOy9);-V zb5~jY-b-9De&>f841x4=LlE_BL|hn;hQ9v0KLV1iLGAeqYZXN3xj4N$zmE6brs|eq ztS%9NXA3N#jMtlWyk{b<&yn)mo4<>U-Z38BN%wMLadAD+?Q%}SA;uV})218dr_ayK z#Oqi0Bh7i#FQcZGzAoy~a8(ajR_5~xK!Q+mho8ehxH&3pf8kJ@RSd}38kc3nl>5oE z8q?ck7U~v(XU*m=5 z%Xq&<3D9n`ta-c;p;R?oW=cv-i&YpkFPQP_>;_1rv%tny*$wLd{|6F)I|RBlAIda+ z@~!^KC-ItCe%0{2pChO!O?&r^5iN$m&76uoj(xKbbBl#b z8bkk8(;f~jr0tFecS*PG5|}>b%c^Ib=?0Kr$36H(v38d!cwO{&3}iRqrizLlL)KIA zrdV=^M)G3Y&wrd;fps^^XvX?(E12pmrOQz#3>TVbuHy}RoK>DX+AJ-r5F@SAIc4Po z2=Q(65whr)524vu^c`-cVEdRr#n&5wE<(LJNC1}glZ({xna`|$B)H{6LP8D#V>}ca ziY=hK1iiC`dWP=rUVQHs%|pB%I^3AVw4VSzFK6P{)T@sXIL!mHnS?Q(;ujBF1!RA>2P({BvHg9P# zv}RdsZ@yX07bgG$7}nf8UQAI%vBq2ia>M)eMrjijZ}Xv)pskiEFati-KO@#OY5o2E zxN#ZYcyb|AC5X3jG$G+aQua^4imWGE{GhO@CtDLj{Cj_J`8ih}4QtZBI9Oq$mrBn= zgiJrJwtF{v^PR%^F$a{pZDm_#p76(S1Y#%X7Y*sGn*c=t}5Q8SI488(E|$_T+doyVVV@mWqd)l5&++`oD6s?WX# zA|Wb?5c|T^RV9G{EtOOmyk{f!(I4_76#IY|#{}@2M&QhW$hSPB>r%)2E8rKH{15Zi zX@*_?E^n2a&4>y*(B~rFYNttlT&->v(Yb?u%srO8HHqI63(oplSHZddL`pv#B;mzT zn$Lj%-*Lq;g`le;R0Rg76BKFU$)6B7VkE1;)7?o`?UHLhvjp+L?8_h2qUbbW3-rNF zU}fKCn{iD1_nOxxYXC}7s|vW*e04f_XPCdcLWK^CXqYQ(O?Ku``&tZ;UZbBnuiD!U z&E{Y4;gP-sXjc0%7r*U(Dr4QfFNp&77@P}i5PURq1HtHDD8XqF*I zA3)irq|+WcU`%gIH)tNL@La48a)S6(0dSaA@0E%>kxLYu4m#c-es@Fg>U_-YUr5u+Vpm6!-8T*=9NuKi&c~u+keeeO%UeD-zfk7yy3v0WPWdX`^9jcc#+&&)w`S z5m<>ZGJ|cjF-_c!_U;hfZ~pl5!gEtm8^Bmuyo;UYK~U1gb2HXv=%VoU#`ZdmO)N*l z8qMxxL8@_d=Lok5s3Ut>Ed-IZoxj}y6`47yYd`D7DTa8S?N`^h`Wx4$>u-R-nBv;?{zI@+)o z2R}b#1p~yTHIkA_?85=T%t8&ewBdz*l${`_Xhu(rP>lVviUDPU}@99rnNVADUd{iMg#| zbxQFw(TOiVF6+#5ApW7u)|c8X3px| zw{Y%ZO~o7G@YJP8((-Dn_a1b?TKYGt=fXLt=YW;~%%8VG8zOZxXg<2btGE1JJOz_q zBLQ#I1&flBkuML9SRxr`wagE=*Sr=Rycjq*CXvHUP^KJO23<1CiBjKIBfbX4mrj?B z0ZMZPD3HT6k0wLMeS>i^8W9IZf1@vGzznlqf&Qfpe~u?hk54oBM6MIwIWMl7_ougm zN@JmNB3@612%ti+K}97QuzCEizNyfQdZ~bC86FC&%Ed-dZ^%1*QaI^Dv5H2T(|D~; zF9XiYwPsC=@&1yPO7aEYt5G7$uIFqVhus)_=7F1MJ; z6Ig)t_N#f*0p=7~rxut4^6x?C!e?a(*X@V4B#h!w^Q_hu{}6t+s-pSIL~j1_>z_bM zZNoJ!pTZjK)?L-2w?Fb*A1~q&?5E#L94+}vY1!2;_hiHt*mERa(egy_cMuz_#a^9) zYcK_KL@A`baJXSM1mF)40wc*9?iA!0~fSM0Q}se$n+pn|9Kfn z9f_2p)fXwYD<~4#rSJaNmIdp02^KUs|LCa@6}QyRW)R=GRg<=LBd?hP*s@Kpo$W5Z z^MpKkUvR6w4IFx2M?l z*AoMJi4{iI``zxgA?YuNBHtTvxq(i1@ez^Pi?^Vc>NiSP+dW?EHuoidT!!CI`;o|V zbSuVpQp?@URFG_`sHrig`<%I#JZ+M3r`jj$knE@vW@5Tm)-6Hc53h+&G!3h*Wpcj0 zY|>SzYpGaF_FDM<#$iy-S#4uPwcM%IOa2j7G0>Hd;eTi(E!dB@C9(l6N|s^KZ1gEJ zlM&#Sw9UvG3Jv`=M3)1239ROXaqCIsb+3qN_;Y-LM%3Ol{Mh#!kxxpbl$a5y^`=)_ z_v2ziM%OgFa1oC~8ypi~2{m`3B|sZ4O5@^fh9e#CMvB*1pyy9{$&*;F#MPe9(&A8E;nNdp;eEbIX);h?gq#f1EN)H zUh!VGwOKTt{=%&dSaWq4J~bT_qWJltZx_{hU0f!z5LLJb_VxQSMyF~h#@Yu8>;8Vs zBpMRR=mV>(8%b+T0AT=g=M}|HQ*+xVj|e*8DqijHxq`~3qC1r`r~;$2on3N)*tqtC z@$Nq^uofH$1D>dWL20lEDlPnOV>*q0&*Ic3zs&HdG#(vm^?a-uvmZIaltyE>t%Uj7 ztq}*{byh$XnTG$x&HqYF`xN_juuK>dxkLuVz5&U9@q0ny0zP5lJb832g+HP0?l;j3p{EPZ#OT)@#itGYaohFcBD_oN*a$%SCOjZoh0 zOvMMZ%ezS-PSCGJ;;d{UW1}bl?87{T;(+>_1;`?qSr_BiMVFoji#y^$WEYf*xuEaL z>oU^o8_!>u9Mf75C_24EB2o9czkmNO*Y+!8cVtp|`o()D2+*$peakt64lJ}=>gmy@ z5%AyvnPwvJh*f=+f2|215Ko-L8plCHNs`!xHV_BRbc1Ib=o-odK0Nr1aA;XsKX%zY z9-h{jCWYPA6}A$?6Y8(m^Q1LKcV|irFu9YYNIpKOLWhXdYo$&!uX|eVd*j8D^JiqU zFZ7Ik&SW)#(mQ)$fuaPkAS1x@v?RN3(+Lp;mt~Lw0vl9~y1PY*+nsir8cG|$j-(3_ z1P1mcWBqw6d9kM4c=1EQL5MdbAgz$i=ilr&3S47{#b*Kpjxm$Epv| zd{)c$^f_R#&Qg)51)xJ2uI9D7qBh$^5C%EQ)| zipdJ*K;F z3>j*{X$~stYXK~?v=0W7S14BU*at)I4Cv+>kRb%-dv_%*0D6f=Vnw%Pwp5 z;GVOCIjs%6Z5-HLlZE_ILFBRL_ynkkI@iuQ(7XZoyX0B_*>KGqtHrTB6(AE7Tnp{4ZdaPLmz;eIG5m-7z8t?%b}b31R3 zGBBinsNkU+iSInb3&h>ti7ZHe1qr8?fPj5$+9-(t%Av-Na9J-yQ{xB-V-8 zWaPT&e88%YCeYCFPd-3N)sE^N!1^n$Tlm3@Hko<0VKLJAo+^_pa9@z31bfaVhy5c) zxevxyO)b|6cXA2~^?{w{4b1|~nzqewD zkIEqv3+0MVJ6{HO?sHBCz|TaCM}hi;vWUNm8kleA;IsH`_pcX?Aa;mG_HG_-3z-qN3StJ{Wv7olzEtqp|6_M}`E4wYi%IVj%H`jA)~$CjMFLcy8$cWuZL-=sa5hgD-uw7 z%102y>h{ddEpi-hiXBKz)Rk?IksD$;iAZa09R3j>CSMbm@HHs~Epv7gm4uhuG+mt; zjlJPQFGu3j&Y8B_b{_L!v*nR6E_qOu=58#fpA21RKb&1iKTq#KSFUo%3^v=Auc`^Z zV$&xeS1%(uYGUIn)!nfW>XX%-=6Cwp9pfn8Wb-Ody zMD)b-RX2*G=E%Yuj^`@o1&yXwBox?My5lkyE<*>w07R>z=xd40-y; zec=z^K=6Y@)b@V^%wf!I%Fndg_=sLzdUdVP&80f9sF zx(qRm{qd`_9RQV}EH$_Qxu?mHr4_~4+hTm8f^HiC2WsvFkxgLRUtnW{_Taaakc%!j zB9E?lDk|#dAaD}qS2Mr~(JWce*8UJ(`x|2CVYz)r;mOmdo7k2<6CGu-o7l|Cug!6?*5L4AGY1jd7Cdy&Vhy*d_ zoL5opWL}}zXg$8Oxq94OlYU0E;g+_}@40MGfyXdnEl{0VXe)EQ zi^JumtWjfI|M3nl8;7STJ?+;jJY?B=%ThW~d&>oOh1b@RXy3pwA0U!|5tHUO-KR%) zA+_EI+%($h%^y6RE%bi|SC7d^NpTu9*{F^%73qi%lVnC}42OTe+?@2pvh3{Z031B9 zCXUCTEkw;Kt!)bQi`F>Y3pEY6zRIDIbh!!Kt?p%^e7ejJKZqyA+xP{(>4cBWzDm>u zz64;R4!5MDBAco88$1NgX8B0~v+X)2lozjm(c!r^Z_!;RU%BCRCX^&;)(3K+KV$S=PSS)OIq}QAY z63<#tz%ugP%1fZ774;COob@}QyBV-25wO7F!p?G%aJ>AB5bCBXCv3U<>TnSC;s(^w zaT}eYHXdx>PhuB2+VcQHw83QdnUDLQKrmIXo~=IpB3!88O3y5SdY4(5&e=OaAVb z2q@Dvzvnmw2Y{j_&X0Lg4JJf>Y_5_LM_=Kz^rGV8sxUx}#U2z^%ga{^L9Yuae};TA z^hDRcr+$Zqb#H|Y+j2QKDW6AuDydNnG?x7g?Fs|VKp(W#4D*8nLM%PsK7Z9dHc8iF zFN#tHN05d&a-UcknpnXyAIBRAq0fYE2vq$7|0m2c74%Rzr$+?z_s4Dw^#D)qLVX?@ zqF^0(7luBqr1|Kn(yPnQUkwJm)6@Q)w@d%Q@PoGoih)A^-Jo4(FAC@>a2_*;EPG10 zaS8+Z-;;4%TWc`Ria!BG{t)_X7J|XW)qA#Vty_wmg3-?iIzk^B++OekFd+x+a z@Sbx~LoF2hE+fF>ssM(p3aZ`O!z}Q8oB5%~5y4q1qH;KLh^VS_6QCIF4|?$u#Au_E zG3!x2*Ze#4g$$4iJS(n&b}E}VexI)17?c2x*MD0&487}S#lPCvWY&S*4(ER0NeXp9(p^YH1$KLRvi=o zTHs>-`dD9n0$Te{t*VV1-?4-+J-_M0QqS#JjCQbgaU>lVSfB0uMLMk*Ly)) z5&yh*Ar{)O;x3I(i&a7O4hd69U2SdmE)sF%?ePmq7$HokvK{mF(r4&UOgnlu)O zKfY7sgRfBS9t-!6yaWeImnl8Y3Tq{;Kb2Zl6=m?pD;$x`HLlc^%Y(PAhYH>*&Cl z37q%Rq~ib>+XXd6Fd&i%<(AWi@-$$sR*nd{TcIpX`+Z7Li{aO8}#(P^YW9M zo1w$kv@P2M=ZVKFl2Fa1f#XHqlb$V~j~ck>D@%;z^kMoRic?Wxt`SkC2w+c= z+r2?~?G03Ix@*2lWbAiV{IUn~%=)iPuOnL4t%nmecSJYF{qXm^9@zpH#Afo|$$Kf= zmUNskhm;V=SlBH^&8(^JZ15@C7O4P-Ywqn=6`tG*xxsH6*jhE`yv$6Le<1j#&4Y%Oz&plN2%Qx?%4lq@m zd=)Tz5G2*zhd~#z=K6-M7xv|eNhkqhYUbaZc&DHz5@(nw(xUSB9wp*o1+sr`4TBxI zqY0wiHs37#u1D2!Lm3&h{``HG8#e~s6sv{CUbh0TdLcVRLrqOlih85&K9v1?`nmV0 zzoxy5_k6WBrx!wOhU!CKT7s?hXv>~7R{G?P(3eB-MVOjLBCiLfi9~RpZIZ&BTdAyq z7kzs4JpX2r-nFlsmOXATGG{H^a&M4k;qN3`;=jE_OZ?9HvNBS!h~W@=?JJ?fDmB?a zm+Q0|oN-<0z(E?JT64q>#&>594HEyYlXq7(ZIeD@ z?x&t*jUYPlM>QzacI!}S-EHF9XFe5IBtD^?wrNKuJJqm zi8;Gd9!7Q*MBRPvYO7@kbW%ce)W^c6nCwsMc&AsY|7t{Xmv?e<@)a=zXH!Bj0P+>w ze2NLZ7<1PZGLQRFl7Nd&uS7!l!YBfkzkmue48Il~mpyu3;{9uq$Hv?1AB&B7^RdcmWL{)>Ff#~ zh?vB&m34N>8Lwvu@@>+krY6^~Ptv;Z;@~~W)7cExl;KEK!DHM70rLP-1{0Hh%x>i*Sc2 z+wM6Bw{WsCD+tZr^_QhwI}&(lc&Y9+=Mty>?lo%_AWzaog7MZgfBKYpGAhblvDQ6Wy;>=}~ zw3ScI_~$eeaV&ys&Wk$}wG7E9`A%yKrBA3W&9ak$|0JD;3M&fGlk=BOWy#+)(^<$ex*mu~TLKhCh=l-eLEg{s>%kpjkxHH{mOyxty$V z{M(j!ICEX=xHRcT(C>?46kSz#vxG7*^qOd@v zxPf8p76}cF&q8XD$x_V3_8WB@2c66%{@V`q)L;O|w3WhfvzT&HLL!DBh09zs!;%no zlMp{*H=n7ZyZ%SyaF<($Wmtq|RK&|m7prTs8~iR;+a*S5r^1ty7f6~m&8vi~DF?X7 z0bN9thjAZ5sEU*f;3QzO)#0^-rtv#cxr%~?$kR#{$}G58m#$*wg9wIreG;xx0%N8M zQ+o~Khjg#6>f;mO^$?-xj(HRO*oF92juzmDd&VxmI*g1cutGM%t#;B3+7puoZ*o9D zyFc%>fgS=Hjt<*9v4^pua=Lvq&2=^X!2MQ-VJv?Ya4+#=)o~)iZ^^T%(R4RSz~|-g zYY)6UFzAAOuX;gXMl+Z5%Dq2h7tT%PN@H?;ZYLho6MAgVwqcUWK3(a{C3c0k$8)7h zHd}5FD@OEz6z%?76Ei1f*_`3SV@K?=hePBk(o~^|Y>~n#2^Pg+C{0>gdU5f?-21Iv zcOwM#^FJ^dyZUvfIU(rQM=!zNHz6b_A(Rmu7YZ+6J$=2o3EjekzRq*q2F8X>47!yO zoz9@s8^mM5|EEIq-jK+!l>fiN`IQY#O;|cYJVAOsMJJ zxbWh)HX5*y&wfwe1oxn1N=)3|u*j%TNQy^AVOju-X%!f?? Nj=i&8BP;OG{{RYczit2k literal 0 HcmV?d00001 diff --git a/apps/web-ui/public/apple-touch-icon.png b/apps/web-ui/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..286183ab07274865dc76a37e05dd9ff62a575b52 GIT binary patch literal 2770 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 Y0O}VJbn-$ql>h($07*qoM6N<$f|y!JQvd(} literal 0 HcmV?d00001 diff --git a/apps/web-ui/public/browserconfig.xml b/apps/web-ui/public/browserconfig.xml new file mode 100644 index 00000000..4e9d6286 --- /dev/null +++ b/apps/web-ui/public/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #222452 + + + diff --git a/apps/web-ui/public/favicon-16x16.png b/apps/web-ui/public/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..f29ac0299417f430854be8aca54d195880370487 GIT binary patch literal 855 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLl<6e(pbstU$g(vPY0F z14ES>14Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a>7NN~(cCuCi*7l2R~`p{R@`3s(kG08|E6sT``T3{t72f>7`5nqy;^rlA$?A6WYS z!{@KxepFOWb#%`9`t|#=(_5Hi=;Jc_g%eu&%r6n*fimN+*>)IC*?|l{DK+OTMs;W|5z>YM}lfw^TB-& zKY#kYx25sBO8ldD|C(}FOw|LbWK8mQchL&ocV!-s!&%@FSq!AEgD~U9Jn`*7LG}_) zUsv`QY>d3B%oVbmf#J-s#?!?yMB;L2e>-1O0FSFS2giyBF0QLJ@(Lf^tN!<&{U-ad z1HM(4%V&Mt${_IY$BG7x4jXkwO|{4Io1DHLnBBou$fL>V`6-Kc?Jj;3fs@i@o3Fkr zk-VVjdha#=oQD}U3r%<>+3vi|*%z_yTE|TJ8#!<9{wqj1kX`71?q|{7O5L0ne;a?) z?ESmYW~cYZjeGJpPvF*{#;&<#S0F~>+1H%wQV=H5GD+6O~0|P4q1I}5kx+ofQ^HVa@ zDsgM5IBq);s6i5BLvVgtNqJ&XDnogBxn5>oc5!lIL8@MUQTpt6Hc~)E;Z-3KB|(Yh z3I#>^X_+~x3MG{VsS23|CCLm76>}bc;^8O^)6h8OfBKB)(;xZee9%@5v&} z!U`@8CWlj)l{bedoW618#E~;cWR9?(Ztz&(rN{6}T(IPmlj&5T6%3xPelF{r5}E+y Cy*=&# literal 0 HcmV?d00001 diff --git a/apps/web-ui/public/favicon-32x32.png b/apps/web-ui/public/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..ff8fd25631f154dcceaea8f530d8609e2c82217e GIT binary patch literal 1259 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+0817lBsPlzi}frEpCl2V|O zYOtbekdjKUl1h-0GL)gL8Yr&`WJ6G}feGb25-P%p&RFq4O_Zy!G)d_75z*ckgLVZcj(&A~lWB)ob=Px6BI)E`R#;wS{H! zl`D6zUw>d^99LC6{l|}AfByW*FX*ePnZ97*7A>8Km#^MMM%C)-NB2)y9vNNh;FNXZ z#1(VPq?FV)zkt$@AHPhWu})Pz)Y2-Yp=r*C51*$_TcfTSR#4dQeO+=sFh2B4g8YIR zSZwkqZM%N|-TOSr{VJ8Z_92`|DG*(nU5wX zSOK*%CV9KNSS(;)dJV|oEbxddW?&Fr55kPe&-xAn1=&kHeO=jKurczgvOKF)QwQp4 z@pN$vu{hm2IW?LoP@wg_&@PP?35g0?YE8)t-D7uKZ`{3m_v8NEn+`78Fh}3`{F&Y7 z=gzI>PgX2sTYPfPgcH#nIyFg6H>adLh=@2DHt*lbX_Hocx%@^Xw6A36d`H766Mx=5 ze`?u`B9-oj$t6<`N9e6*e^PkHVb`g2x7q_oeTu?@qr>&v+8mNu5>nGFvzcZ^cSI>} z*_z$J^laK@K8CrmcWw7NL$$2a6hn)DWiYE z-r5HbCiL|c7Oj$bc#$bx;GDi5(}AZ`tx_h6?bx?-sdb_7cm9X>OI9C9*08g6uBg1K z;}vPT?7q`SS^0w;swJ)wB`Jv|saDBFsX&Us$iT=%*T78Iz%azn*vi=4%D_b1z`)AD;Ee6=#V8tb z^HVa@DsgMro4_m&)F276Aviy+q&%@Gm7%=6TrV>(yEr+qAXP8FD1G)j8!4co@T!oA zlAy$Lg@U5|w9K4Tg_6pGRE5lfl4J&kiaC!z@o*G|X=t4CKYhmYX%GXmGPhnbx3IFX z_hb=fVFi~4lfx;@%9}$JPT#n4;>ejJGDp}?H+U@Y(qnifE?Dx($#g2v3IGzx6xz66bd+%-%lI)yw zcjnG~-+aIM?l*J4JKwCL#3}uhVZ#*49m-a>qKr@!#o-9t_g9n(s_RcV+GpIRC@Y9y zAkh$ul1Ufpd2;!)F8s!MIxF z6Y{L`fbU&sMPqCe_>sRcKid|^tcSqGJHkI7BFSYi z8(01^A`I{SdJTi_<#eg0nk|?xJ|h0`xU~KB4336x4;`0`Asqf}pO-D1IDvWHnvQSf zGf%Uot5;=qZdd3-ri%s((SA#gpEOyp+x|FL6-Ov0|#*Rxk={#+je)-59q7%rSX&E}+z z)9I`*NNuO7TqoVz^2^}N@Xz&nSW{!8OvcL}Z-1Zp2FC0CuUj~u-DqtMqQiae>{&WZ z1i0WgG)6$D_bCfB!;ik6PQH&F-M3FB<>gJUtknAf-An0@v6`y0L3B57++ffCASH;- z0^e0?yJ^uL(vK`G9DekNISIR#Q}KZVA^lAqIh@_NeqAPGZ!Z2t2wfPAzvpDap(}Xduu@uZror_yfE$&9oOUE8^ZSN z*d9WMd5k{7;5F96e!6Y`&h@0T^QTUQV1lpl+}MY7*|2*vy5olqvT64w>2wi{p_XX6 zyne!;+5D?l80Maw@Xp;`P@wyIm=kX=T@peE|7hEq*E{1EVg2m%`)mKMzcZ|tCqDgD zhRw_Mb#-jnQ&V+aP8*$Mx+cM&otNR&=_1g%OnZ{(k43{10@S49zx)2pfLHXpWNId> zudNNC+_&?uY;xlFbvi*GMGpU=)Bc+g5Z1wWmM_!&nel@MalhZ_yJ0S4-H8Hc7+P(= zM^itDrk^X9FB@U=`>OJ?koUjYI&|{LXY82=lEdH$^%t$N(cZn=`5SAR;6DHK)|(;t zKisy}bj|zU=SOtfcABHx^Vc$8H+{PVFy}|R@AlI{32axvyWB53S5Y41n;G?h`3t+R zJaK~i)5i90xX(^^tDbJ?rRa#}|h#m+Nu&X=*H?7QQR zZxa3(h#xu25$iJI$33m3_s|&nbo|caa*Q&=F6t;2lTKr3>4Ub^jJt?!`Yd7$^Pnj9k)q-zUByc!0Xl2bv%Yb z^A3GOMl_gMhoUTIio%K%r5|!I(@z`nWJPI3K0r|#kl(H-mB{Hdh{%g@-%n9Qo`ZYx zO6+ow^`(>pFcIJ`0-ZG8O598O5PA}N6M7VS)%6_EJI-l2=t`k0j+dy=pRNc~N=nLY z8JaelY+xqMk=6e1QLQ9!{^nA^mbh;lC|~lo5eY)euSSUZ_= zd$Ha6E62c2pFGKCKk~57kLQjATmQ?3x4AAXbglCV8ovJ8ynpasL9DPKFP9>Z*Te^_`bk-TY3)qe>!aR)mc58@HsES|AG(5zi%ww!M6ev z{cq$o$VTdE|C?lA@eCU9UB7hyFMV%i&^UYK`vc!uvTRWfV8EJe^1QlUhjHUOOUN_J zp%;=5zpy)P65q?YK8NEwW-`s{#J2%oA83ste)y@S_WL+$DW@f#bhkDO^IO=tnH-iA z$7oMs0PQc_UP(I+Mf9znLt6&&rF1gMeV|-KeOHnU$bwAB#y$nMB^-3=`xJauQiVz4 zaY4)IsHrKpAH*_INbs5}&5IFq7I^>5A08T4--~3i=>D#z)C3 zWV8SWJ&R>g$Q8UiC%b)_-qS@g|ICMe7(^p{0hvZBe@x{dKLclCDPk5wE$9`1}9 zbQI`2WPAgJs;J4E^nu#zVS$ko!-lgAWIPT5dO#Ve2albyoDj zZ8Tqe3C;xKkH+>zAFY^kd=9wPsZsXPia7^fk^DHcqA;kwIV(1nKUScIc8uY6y{zp&O(GB?W{5fuVkMhjgcabdPj{ba%XQ zy}#ZcZ{2n7UiX}}_dT)qx4(TNG}IIbfD}Lg06?ImD655%asL`FHtM}Czt(^fnATFN zQUE|zEdIS27V4YETv1CE0Ptl30D?jRfLl~o&<+6L!2HGz|+4ctF<5j)q~@%q$-EAgN;VOh!r0It^xqSCrYwXI^J^!=|28C(>Gr;-Ht*- zIpRW9yVbg0(Did=F&4_n;fB(LTxutS;|4KIytlu@rWTk1}qj58!d!(JTpv4P>i4d#2P}e7{n#mHjX_df3!q= z?+SDiV-fPxBO%JOt$86`hgpteh%t*6AU|J3%8r-!#FL$Hl$=6-bJa$QD zr|jyZUr3XqoqA?vYZ3;=I1?dR%;4>E<%&uME+YwWKUqktpZc*_;A8S?Du@ucp^+8L zOg-_H1#1S675w#!kilNWiFP`Wh0V@}UKLX^I&m^c$m4;B2`)^~V!T_?mg>R6TTsqc zM2zNfC{EX7r)M4X0~k65KVf>V2^g~9sTe7X>a5>gTC-w5$mx1LncGC!vl!HFJ=dR$ zeIMK@)J#b|ldEFpIMTp?+^B10uD^g#iAEo?kld7q>dT}K^k~+x7slr1F)}sh@YJ6} z;^XlWG_EPb1*$FIQT|se`;8Dkc4S~!q-B1(%V;h4am3kHAmp{sxdQ!>CG1h!PhKJhZWG?qm^;<-J zY@`bNi_cQxA8coA5mFt(pCB487W1BT6QWs9qbNu|Isf0B8lZDq7k@}94dcL zUE9_*rw}H=1R{rtB{)bo<#W=}GYNnukOw@An05$-j+@phcW$!!#AX zm9iCbNyQjX^?tlF<>_4?6sWgNVzyJ(fDulW8~Yc2)C}la7+v4nU?r(lQn{mRFHW1j zgha}TN3y;qwkahNZfi<=4=nUL!V92zbg0>6S_(mkdmpc`sSoO~W-Z+RbV;O|MGw5>Lhk~i zTf#@7EzYDjnKd;M&K}sJ4ABBv*W@uRMK&nEf}5BT5xE4T1qWE86VIlhG3 zV2!`;AHBkO;{>oQ)<6{RaTOuzpL$cSDbO`uYL zoGwNd-a_7B1|Cgj$EHJX9xM>a*|Z^3|ifqVuqd8`Ycwh?-jE_v$+} z#PuK8Mf=%I!UD3@9DcJ7K4{jk9UFMB711}_ePoc2h%0|7})M5f{O?B7iI^kJ`1jAG4gLx z`r<>9G#<%K!7V-Z-j9FqLPLe?Ss|L3vC-5@YX#gB<%<_w)64bhdS54#BCg2mBkEs| z0|U}=PASkX_U7S8q)F46*B*Fa7w$%$?p$)nhmMYzk7Jch@lIxJll^*0^xB?XCO+9O z>krV9HlZa1lkD+i6B8TzcvSE8_VQVQ=wbp|F4f?~d#88raCFNNgGj$X==Q+|mMlQJ zCPuBnr1m?kMsU+%p>k|fPeZO+EyHZL28)4_%0*cH!v_FVs8vZ{pMLQopz6`@@wUFc zfAB1||DB_P6s%ldS+HW)t)jU+0y=0vT`i&_8IYkA8!Ppm%B|n?8!-eX;r>a-0 z%n?P$-1O60egLn>oQeEL;+Z5nn}i>d=!g7sKV0+Go0ktP*&?N%9^Z_$2fnEb3=9eh znOmeQD9GGc>2${?*p-3=wS^PV(gyeR^{s7h_mwXW%q}3}K_6r2LT9FIWW?o`CC5vl z{Fx1_^}>md^QwN0Zac&df2!5J(u<3axBKLOp8q7Xou9G)lIkdBQyoFE#NZ1G&z66s zJ(4(JzE|TP82I+E$sPckGwPdx-BlUF1oPXytA4yMX{YC|hzrJO61k^n`3~mnYP3Fw zZDeq-taP4>ev((G#d<{wx>bn0_@Q}HReJCLoR^o%PL(~+VtlPOn*2^6C8fZ9mYifN zE>Qe!{H#g3+WN1~roj_1=6~@NV6eoq&SBIX)QqnnqD$JqwQ3kD<7|0%b@i%l9EYNIn2TVEtj)8hKAt1 z8QAu(*Xa!=+fy=)-sv}@=S&Q_g{!OMgy`x23@Kp!GvsphwmbH1e$^Y+u%5i-v1&_x zQLhQ=R6!n|AR?FcY?t*9p8LZ3d*T`ak61~Z%c1|_`d;mZ{I@f&GtKuOc@DATW={k^ zrK*^wSInMOD2*`z!O>|Uq~@R3P?Cfw&s{z3E{ z7&@ejsoZ-0S$|RAai%$zTSHJccb|E3eVSxqXm!f?C$((-(lV4T`IRx zSSeI2Gee?vD^Ej0E40we%s8JmboL#S=p4qUn`yOglp-dZtsx;%9HvzRn~krccpEB~jP zh2ik)l-yiOucA^~nOl+C^4gERO!B`*8AL>6{y90H&qS+#<<3!!4bkCvof`=rSWsNt zmjYy0!@Jp#gHzn!=;p%#eI)4YBxtPiQwSYWkK5rmi)^u^zPqF{ z>*#RA#QA%-Kqfu5{!6Xr`R>5CbR7G|;%wXRs2mWVnAp{k*1T6YGD z3a|BFy!9KfH?c+65#$i}1)FTZOSycrB1uT$l~%*LBZy(kjv&V-oF)Q_YnR#d2O??V z^Ikn2ovz|(=)h<@1cfoe{vTG9Q-ugNN5t;VF1{>okQt4=|MH6UwCHsqol26!hn{|N zO5wOIEs+G~#lcJ_CgDU~<$*x5GFr3qgr@po=|4=3jc&|)84d3A;7n2wL|$IV{FmhL zo)d?M?bRv@Pa+N{MpnEO9F z{1ocfCs}saP@S0>77_c+l}%k!6EE=77AKSBC2)I`Oc-8|a54Yt-O|cN@866%M#8&T zKDq$x!DOci_)~rRL4iH5SN9j=XHjV**)k(ee0xh0kx86sK21ezAAyB*VPSGZhzkns z{E@z`jRIE)!k)i;qOb@+(efzX7@~q zMEx$sZEZ&gOVb1i+(cu=G=qFA_xF5Kh44K@N5i6e)wWrgQIRYJGZaqeW@a8u7sV!{9>RsQ>^(~&Pmg3UY0|1{|57xrq&vg z+1uNj{!t>30t08}u%lS?pNIWU^ifk6Y4MMS@7w+!Li?FlpmFWSzngv^umUrVdi-v0IJGUJf}*1XU~=1+`SLJJ z=4)}~yxV^?Cr z`2TKn8Pp;W{`qRq4phP;u&qdCd4++O2WnBC_J~Y;p*o2WEpl0vCgN^uK^7g5$$U9C zA7M{$&m_h`wr=xi=MSxK^NW0`Gw@JksZ^{%4?a4Fq%z%{^qHUUZXz2?KHYue`LtE? zJk~?!z2$Aqows|4+q@VXUEaoL!r7BGl_-LziDDvJd=4Ppve#`U-;!S-X{+yWDX*$+ z5&Q-}={WOBcg7bz2Z2htsC|6)wby|3=7#Kth&`=oVC&$Hl`9~=LUF7gl3NQ$w+gi7)J&gMpKpVrcrWF0s_&A0?!LsaT-P}a7U{Y(=Hx&TTS@G zoi_PXU;Kiz3_~QHu*;gYfZ0Ux}q-z za041gay=p)me7@-iAdf7PvF(Z(fP;`24&m!Fn{=3=*i$V$4O$1fUDQ2- z;(B28va+(flKdz`yI{jSnXp`Lkmz@Wk&uX9)z&yI6RpXQwCMJJ4t0RAC3CfmT)g){ zvC!4tWUyb$5^VmtFv9a6r)qv8S4%IRHQBM3g@BEKKCHXJLAiM|+<~XH8?vRV!}so9G@D8+u_?ZPyPzUoT(JtwZ9`8F59x*yc{8NF zy?N1r8~^BU(dpxA;=46stE$7O19>5MA1KoG1(%2}N=!iOlrtnhl~1&-azZapl9|a| zFZX?NRM+)ymA(*U{yI8rk3u3dP!)>2=cU0+#j+GCna&1OXruWQO%eg(<8#*Y;@G2w zfGa2_+;ZF{OA&^7dizS{<~VS#6-Hf&7sT`>uYeoM44CtvSd<&zFw24Q6}{CbAxcSY z;xIo^^25+dSFWTMBDI+EtIFq{R=?${5clEv+z4(L%ohh5kEGZ^W7GySJ+lq_ z$4wzY8zwnrd_~lXtmWW19i6^!hN;)$!1uT;W9s|eIHLn;!ZEdlg&SW_qXyAM-C3&Q zfJZ{a33f<$dh4yL^Q`i=Ms8wSOx;LNGm*4ovA3}jM4BxP!8+7)HI}8ttgQse%llmq zd5DyNttkQXYF!v{=HQr#aJW80hs|gV)6!rvs;XrZ>2BNQD90|L;vVfu28EP}N0>0u zl(aWdIv++?L~v_3aIzZsY{u&0vFvGle104@K9lNAhTgbakU3+rnOdgo^+Fmr>Le<3 zVwVe|*17P5quVXsE>Ns6=fnt|ykJOy8E*L-Oqa1qI9OX**=3s zLRogZXtJ-T-E>ckqMGqDppl05cZ!^7ghZMV1$gO#j%R4cn)gIpi-&XXxIoNaS-V89 z|GdW0i5_Wp-u=THjF!efk<_jS6~$SI9GThg$NTYfvQ0LAY6FuJ*K9>2tF#4?Xxi14 zzwnWg-5cl->h;yii|o5*X0ob%kGsqQAm@=YW?4UhMz`TXK){NVSl z<9SZzSndc?*RWJ_?HzUjW+LjS{+pU=4zi!L|L){gIAux|4~Oq09#Iy2Ix9de(166_ zWxqvECb9=cSyNmm#&yu+3|xHuYrcjYY-PLBPBFIqX`%;;wAC!_*4EbBb1ldhGjcIm z8ejW@rm4aapbP~q#yDaTQz?SsNrhKd7NujYTYr;Y;!ufxOARME3kKxO_w>+4<|b;R z*5N%n9kKvspl4%~)ROTbYnUF(=Pe@d)Kv{v;L}ddYbX$|=YvK>-@CMsoEks^U%n!~ zq8DMr8nwvf45WU8Jy4dw^S{?UDhJwm7NTQFZJ31?qmNdk2-#0+_CRdxY^kl#g+`KHpr4Io1b?oEqHZV)#5JQ@5s8Hklp+aAV$aryCs7QeWEIXvig@fJBl)M?9;pLeujglM zf5A}q;f(Hb`tBBH?v|qFu9heP;N#&F;N%hF!QoT z@>_jyNNH0GfI*MXWsXlwPppzafL;Q9RLWI003+P0Q>=~u&V%okp%#X z69A~>0{{=Yh-j(?zJNL#>gxb!f6ubcnk=w}F6h=BUAk2$6^l5OK181h0Jv}8(z$67 z`ekD_ER>6M0^QypWubxf!FqrcjbR9cDpiV}`|+%6%7p~L@^0l78A}4f>gH>1s+&D6 zsXnIVx}r39k0viqakHD+^}|FIV>MyHuqMBd_gid@SE4S+2L}4RpBX+nG`SWywqL?G zuezoTaQrVFPsA;FG%-BFrQ>ZK0vibgQ{uLc7i0q?S-^l>AvB58^wE4Uf*`RTtZ%qs z9{^3wm&BhFt}a0N*@TXyNbM<#PcpWzft<{WLt&+1nvFE6qNWg0Mse3n3D*=E-7bmd zNJ(AaiE5>o->cP^|J8Is)8JSIVOj-n&ws97kEApE%?^=+T{8nVs9u^QN@-y|v|uoP+<(44|B&fmY8nOCh>3VefeiD;_a~hX$1|`Vhus2jy}OTS=q9LK zsSN0JRN(7ZeP96#T7TNBYMCCLlT-4paH$z^knvz34v0|AP>UE~?kJeDED+eIo{@1r zHO5udbuu_!f!nVKE?{!C(m8q_v9CTRmH_^@Z$ITR>nJQPE_g!BPfG1I-QxE0_jeRG zz)8Y*%ZLSl1l4*DyOi;(h;>h{CcPcUibL-u^Ggq_b^0f zT`SM*sU#E2!~S-;)Bc6+EPQcRy9aTGrmUS}%YQ-Agx*QV%IXUTsY;0>hW126K|$Gc zV)?t1$78X94Kp0|NB?1lT0{Tyc@#*Igo^J0FTFSLK?idsVGzRm`C11mdXbcV)skoF z0yC>{>W}Q-`Pc*MsfOSR1`*K?47n(-j_=G2k#4@tpPT_&b@8^SVthH$`Z~<{Kr)09 zOiEOuitneZ@H2UZ2vti45}wp2dRbQVlBPH2f{ZYtv3MT+wM#c{R6X1dZHX)ApfC8b z-68xj(8yy}qZa9co-_@sI+X$t7UY}Nuak3gx+;k{%^~v~m9|rz)%hNP6)uFpMc2ri zv(?I6_}4#qzohMySNTYGyAA$^w7^zFZkvdZtTx~A*q+Vm5%)IxzSvPCR=lR z5dz97mmmAWak>kp$=19A4!Ha3$Lt4Qkv&m} zy^fmEi%Sv`uQ=ghkag7>0{?Gg-~CV0q(mB+kiX3Z`#(yu@FJbn4?>m*oiZfa<^tv? zj>7-uJFft@oRakFlw>21zF0_QTU*<=P?9bU44O<^Fq!nEKK58UInn^b5}VMAj0J=& z5Y&e$+3^;6>;|EkW0klP7cK3P+e`r548e4$YpHt|BlRAEV$D!8Vg%isDJd!Iy6+eG z;|$7q2yJ|SpYK$tUL0eUn?ri3TkD<*kzTBJZv3439i>A^^hu4YU;plZxSdFNGxI8>U-D5mu5Xz zSG(*y%f8~AwAxy)?q)8*S*myaTA5jatcm2?7BQIaEQmT4dPjm993dflR&?^l&iMq* zug_0IBb{L?7~<~y8MtW^i`(}Wn=Q9QuWmgU`?5BxDs)x40M|Bk3BV3Lav^V?gnIrI zMjr;0k9TcL4STA-TljWU;w9tO9z1~<8(}ZQW7YLXy5v-s(YDpszkfb1&VKdNb$+o2 z!kWHw;zep<)$ccEqe84Zj`n_eOjj>=pWT_D@!dL(F}N*&H42dl2zdX5!Wz?VgTB6` zPSTwcjPFmdde;69_+V^#^y|BNQg-$hnP`MYi!q09-8`IP-L;1q9 zz6aZ}Q>VSxey?(@Sw@&9N9Qtpxbx;j$dA#CR>HncbbVMH2DZ!3iY4V9Qic6$*Gx|+Culc zn{$8GdjE{0P=RIeL;1d#T8A=gWcbu}+)D2lzQUwb$b($pToqi9sG$4xITbwv!{EJA z!T5T*l|*yVZ<}#-L-H16<||r!2};+X>GEMAPUja*2c`H zaZT8-;~^B0m;2=-JT^tcWMF0AQ7@gFg?}C2i`3ChBgWvO243rlVtzHq zM$ZszuM(}Std?@B)_zVU@c92Ui(L*ThGe|P`;5p%&#;pmKVPF-#HC$79axYR!GNIF zOOA^_hdk|II5^o_DfgMqZI(I8h;l@F^ugC>Q74k?5_kAr{=VK_lmF19D^GHqW*qVf zbaX-U25dH%oNev({q5UHb0hGcpXR!v&UxFMLAXat z7bZ}Ory`5%KATL;tS&8QyPsbkx}@_e(`D5>?fE>DmP_W*a+;YE4UJiHo(ZH(Lt#^#Sid(z@V@!iQ@eBRW(cO?9WE!b9}8 z=9L~qWx{q3dDTtg3Zvehr7@L>*)Mc&L~K*qi!H9dVo5m8SZjPH&P zqW2Av+1%O}q@h!6nKVAy^C1)+13SO2E+w!zy#1vt`irq%C`KRLOoL*q)WO|-(ccWX zE04#nOgwlHFq+@YKW1dRNjl&wFTc|mG%RoP%9*jRPsWQe46JnElr93$*l7_VO-5hDSnO9=7FUpKj;(>_qfS!?=8= zthdUIsXfK5>B9-b;5PJIRLtL*Kva~vG9QLmb#Up;1jzp06FZDYn?wZ+fQcM7k;kn9~88KY`mqvjnZ4Ra(gvucB$g`ZA2ZQPP#7EbtIBGES8eI}d4T$}`Y+Z(fUGI>cg zB}Vn%9XOa-!3gDopHF#T(uw`ZHrAh-b8hixehjxZeJsZ?TfReASsA^F+AA6+o=&$X z#i#Qdmh*mOmFPk;SA4%s{8WmXkRZxoJ|R0Z@XGye|Gd`|^pfIpi0*SzR=)5~{K&Fa zkWnXog)Gt%hLBZSg*sN<`?^b3EOeNoY{$(jp%XgPZ!$N zHuQ$TwuNbX7Ro-Rra6Bj4UNlzV>#O6t~E4ga;gZ6pBXjeN=at*cuV8og-iJ?$GQ z_Z#m}+!#)Rzx10Q4Dk8!!&gn+lW%87MsiIUnk)xiYufid+q+ga#|G?em<)~HMWIiR zx%%CP@O%l%FNVk}q`r4g=eO@kM~4+#2euwO0BWbWNVslx3d;j3F`ULu9N zl7}w^z@;WRo>doxX8%BeX!2fT{@GJ_4A0VB1QAI>O?+taF&x=g|9nJSZQ{60Vh-FL zIh^jO2l-J#lJk#IYF5;V2+iPq!Sv5#_g|<@ACt*<6kK)-_B;7s+E5Ak^MdaA(Vt~k zL#z9lM?p{BNa-%6et3B#F<;G%_QuI?9HjwSz`0d<$RFR2l|k3Xeuoa?`PMBx1pcT| zXI<>3kXBbNdzyW#tMAvVxk>?C*p-L-^u8FtAez@&jsuD)z)OB7Hl^9}d2z zDb`WhTOo$d-o9(cT`aM8_7W*363o6XsSh@^cJH4$$~k}YE>aG3z>p_Nhn*4oI~z8C zY54`GI&CsYz?aIA@@R_1iG9CaS0ptfW7mnEc37CW`>5@G<-i!1A7Sk0UXWT^q@c+R z5Ut_&7FJgN{V{pDxh^YDujj+VlzdsY0w|^zbEVZ{D=v zaP&imzpM$Re(myYDx9#>&M)ii;EXHTlPzZm??AE|%chnX%<;_U7u3^XPQ2<-;9+t0 z4^Z9E{FNK{Aj^vwaq(qz-jo=eA0i4z1)3}T+cAjLvdmed7f|`4JX=;>Eg`Lq;fMtx zQps<`e0#%VSI=>3pO@X}` z3tBfvXq7pk^DRuxnuUw}`iCz|Ng+yWWzPQ#e)o24-T~Uv?U$W-o36o;<{~F3|L9|9 zYf&OGne7(V>|hpvd-b|=A*c*B)F3l0+*2VQQiZ80=So;aY%zl0%pZQZ)*iy|(6k#l`eDOMJ z0zbjR*x>%H*_-sNnO%)Se*qGDeWZ9Vf`p;DCgeY9de`HPfToZ+g6?5(WyjO-SWrv~ z#y`T#RCe1j3f{s>*B;emmga;e2!x&7xmQaKO=fewS5ut*L5Q~J3H3%DKan>l>)~K8 zE(%LlimcrB8XVsKgcyVHLg$_}os+CAA{2MJy`X3nfj>!`)>{6QbgjcK7l8G2H8W+< zNVwmtdS;HmWo_quWUE+uz)ABB3LtAyo>Q(IK^qr8e|{oRIW)v85DnwZn&c~J;UlP9 zkjFIz$s&4fvrqIbdA{nX742ok#VuL$>(8DTFHtD$z&@6zVl9o1ASLzIOYi$P?|0T< z3gjp5kxV0gaLhmvuq}Ol@X4XPnnjxMXOJfivo3P4K)<`Mvu7LI2)Z83r ze>uA`H?SY)4f?ybsj1|h_;}Q2yu=@^)Dkm_x~PsoQ4G&0wTQWqk3{FYZl}?mHGB%jLXu4B~%ATNEpP*F3;HovmA^L5x zpkQUlxYWVAoTzziZ~={trKW=}e{a42VYlFHg7%AH${xg)7n_UqmchEi=;L+d=|3_O zkT7}(K$QTTKhP+xZU;Ud4|2P5l%0^Vx=m&fH!`__N=L3z8mpNCq>BH`jB^GOVd0Gu zx?TDU_Ac)*E!xS3JbRn7BY9LQxc>u(_u3hiEApY{WD&s#Of-uJ>DmOjB7@viTmsy{ z2XIaH8vKf^!WCIL3psfec|{dj1u0os6y2{YCi zyCMb?lfAKikKcQ|-*J3@eE+{Q#~jZw_wwA=eck7IUH5s#+&9u;ImdSn1Ol<>>1vvS zK(t^Gh=!Gk0cb(9X2F36{o^}^cR-+;MCL=sGr)ggCtXuR5GYU_1PXr%0__7$;maTp z;syw``Va(yz6XJLe6lbmDnJLrBLf{x(8=jH7hCccXkmow8EP>uGtjVzF^2gBWP?DL zg7q}-m_46dn?VMfE#~!XkW@JuKQhWavA;r3Pdkd?)T|gqOC^hi6JkrfY67LB^RJx) zo74*YaRDYz!|Gyd`?wn`zyPfVt!V&I&tt(eH|Bb zaVPz%-xu-Y@^hK&_&_m2X8ujDdQ#+tGz(_+;rwQhUan5!?@>aPjWf*HAw@|e&g6CK zntS1T3Z@0eLnrNCo$Ak@@d(!OiLY+x@ArTFE0-cuQp~x~bhJ#|-Dj9Gw)5Uj7F(pn z-yp;#DQXUtHsg;#Z*q@Up7uNE%uRmCA>ifN>;WOwFMJJ14sC3;!o#UjjVbylBI%qX& zo>uWfvG-g}I?QQ3b#xB#{(g;rh0<)6AhpF{$32fG>C-i^`U~<@z;p}2ExAy^_VL-4 z0F{X{DplG(K#=~^hKX%BeQK&w-b_vTvT+qWvNKHUe*T{-r9{hm!|d)cUG+zRC(o7! zoMh3UywmmhenSmRYyB&ddw4i2$A3Z3h2s|<2qZn+-yPsxpzaEqSWn9>DSGykL*)Hx z?R04VATJVWK!+MR{l0IV>orY1iLl%>?aXmWuZMaopJga5taLvfaPkP>ao*++ichA- z|Ha-DMJ3mcV9rk?mVcz3AOsmeB*20^@3|~F!njL*y=b|$w8B%J&~+)|op5IzUm5|j z-{i}Ydy4ywg}dmwTq76M)i0Y{=}-Ly>2+PRvY3_M0sG62(RSc0DuTF?r1$Uc59)c@ zG18>)cAxPnt^IzIQAmUK%bl&Aw0Jj1fzl`S|1D?o`BnGN{4p#z-0f+8yBukK8g)=i z7FL&^SG)I#WoyFgkSh2j^eXDN(|n}R5Bd{#T2|-nz7|`Rpi*irwAFZNd;{$S>W!YK z?V3?VRbsy)&M78H_*E;f(sl`UpNXz(54mM8Rn|cka1)Sb>0iFJ{)h+(ch>NwneXTO zX9mHbc(LMxoen64B?=Ou{QcXv6$$)Pu8tr_M^)<4=9a^CA7FRf0>JQZnuopl^)F*K z7VTC5+wTDfE&!Vg#sjZ_#4@w#0NynU+~fDd#09qEBE-`(GBVyS0t2rD1GAnJ_}8Fx zFMjsfuR`hm({yR&AGCt7Fv0#8ls4(QvL0J>0Nv!}r+F z^8qrvn$}GTtZ+eErdCcOP54Izg7|35g&7RO1dp;ahnW-|zPM^`Nuf-kKwY(v4 zAW#*vjO5~>c-K;2v-Ayz>=~K3&e^9Fx{VSu8~EWTbR9zxbVWwQtM@G;fn(|f&qG0M z{@Ei8M^@TLb^}5&n)QE@KWs0A<>Z3xprPw?rlwc<6h9{RkZeoy=Z_{E*9x@p0R$M` z&ToZ)3bJ1I@o@m^G2HqjY-C)H$1Nx{*?((kHkuvyM2H(m`^AbQSlNAXZ`80KO zVC=Atg3;y>560r;s;nCZwdNo?CENPMeHLVSS((?LVB@1lX~QEU?SJM>AJ~wGe^-|~ zef1CA8ZeC24zcWGKVMJ~#rhQa9dTZ`U)hn3DQj)TW9g#E>*aDi9%9g8v#&_QY7vr8 zzR){zi9ELD%yP-cBikuX4imU3X5Ie#ii_*+vj)Y_f4FLem~>SDcdEl+9M<~K-m$}g z{;ca93sfa+Pyn%(C$wBrO+WLbqzzfQmRXCuks~w(O zV0+TjEj_y5gvBCJ-oXpf)<>Jc@t2bCTPEDnt&+p-LXyraLwK0(mw+R2a>MjIehxmz zyiihB4^PBRFKhSF(ye!Cggez8H5)G_M%$?bM!ws@(HI1oeMDt#vJpTN&o!ln)Y(AK zy5J5Zs@XHnE@=5jl#MM3)ENtqC0xaG#BLxvWf-1g5GR)jU(~Xu4y&*T47TEGbP_F= zV<`g!S(NRSxhA=R1*GRLJ z=$@rlzkW96@0=MYt)M-{E34W1>?ugCcPJte6F?kXFe1G_&qO;5`v0_-YIp3hq# zR>yHWiGpti*ZhQ&0g|h~+8f!UI;h&oR%cZ+gOv+#zB$3qtkm9b=7v5yspj9dE%Xla zY__*oa&UkIC6R?%P@S##2u*cgnitMbO4UMg;HDBst7WiEg#+sbH{ZjlbSuxDZG{S@ zz~5dhftBie>k{t0^9PELcN+`?*NxaJJF$2gA`m)0vLBD^4ZBo(9CpDnZ-zwN22b*A z?e<(UY+q%^{+XtO?u=r*9c#U&biVV3wWfiCl$MYxlo2HAV8$4(U;Wljc4~uI!28=T z7h>=`5v;C2l=S+cob;vS59HQ7qZZG19;oc2{}Fgf~}=q;_*p-aj>5Nq%llP^YM0@U;%5T5eFU*U~6 zKWkk1kX6$i#Txq8hA0>QaXy-vEmyOn(|M$r7Iu(mkk0k zrnLQpFec1A_bd-~%Mb~6b^ejy-gX$JjpIPZc(TcO?1j$L;kK3sV~t+?#J<0>>_Itn z1E&$w;%*LwO%-Q2OQ@X5rPwM~-La(8Gu10`2RAQGe{A-U4%tb!;KIAO(v zCQQM<|BQ8OU<6LZPyLXG@;unUmxfjoI4Q2FJ>j#?Co55GrZ{C4r?Fc{a?u4-Ub z;hapnC0!nrEEx^q__?>$C>^wY9$UYO zXUQRTtQ)`N2>5TTu&4ur!|Fl~zVfO4OkkMlaY$(o4h*XbbVt?dWLuJ&Yispqg8K?G zeW#qtOO?3~R2*&_R#@QFeyZG&zcN8m&j;=`pU(}HP{P{dPiC=iq@%9Ft=(>U?=V1%ZNI{@$n9W zR~Xf0Zd5iWvuN~Y)v7>~AL(g)&hsc#@M<@M;39R8{~3I1n}V^*|1WtSzS=abtlusT z4PH>0xw++=nr_+CcGM#B+JjJ8VXTa9yjdP~In?59b&F-tUU%p_5|E`3J-j8|NW_zC zdyu4Sr!=^`Wj@5yYj>Opz(R|HZBknDlI{5Y;rsXRrNnU-{+p{QCQZv1!h-)PhFra9 ze9Jb6gJdpu8Xtk5RbCj!!QTZe<}kLk{^3u*SZ#8cEVok}kfp3B8&Rx+Ly71x-M#69 zzV}OzlZukB2P?yQbEI&C)46&Z;@t83frl|gnKw3!)~3QrXQ#4x7l_IPYt`20RovHu z(X&&US1<1LF7x6Sbz?(VoA3J43_8!^r_d58D+|g7kha|iRjNDM<%&1#FmN~q49a9Z zg>DrYv~kl}cP=ndHMp?tj^BBq+sefoIRe+>N$jFCw;AVSG&0pcdWbc+ZC5DLxw{K) zzdb;Y1hlKR%}#Ot;^|f~#x+;F?;GxP^tWIz+SD^O_z|Z6?zu({@{xC+(DZuc_yGNe zDxG02dr~y(WYgf02dV3*?vp)mz2|qhVjMO$RKVqH%kqCouzIT|L2zni9WqB;Hozw$ zr(}fIN5eWcfsBG0COfNc{H@w)58n6o@GJ;~4{iC{b8)N!o7O(X%H*w@E&NGs=!yYSG3g{v5&UQy@&P|wZ>x=JP`~&kd&53+aNSS%A`oH$lvLzb zh}#tc*3IQUrdgZVia{TJrklhdEVCDYGd-mrKN=r{yzE(Z8P#fKe6(5ATpbU{jEH|h zKJ&p!JLfWnjPv9(v$m+VuZ9Gul?{J!K_nO$kaJ%#7n7$iYB-?^wtX^O`>hIz1T5J( z(U zVhQen??&ijGH{W7aY_>NMX34%^2FA!_dV5fNhMRRx6!IWw;?~rPd5Y$u&v^jd2~|k zzL7FVj$q8OKp^5rcH~SOMnLM{0BQ0tRq0raz3(`1cKBqx#$zD6$qIkyA79ykNH8?? zs$vvswx1}#y@42Ob<52liFh(XQ34#7fJp!{WB~3At+PvFGj4Fy-X44WQtLFz#$WcU zK!v*Qoc+2xuw2H~azUxI{uhbI{daRl%mZst$(IKLx}v8*(H4j2wBPcQ8hmSv{rl?V z6zKD6&r%hBCIrq@?WVlZaqHfUdY3UFy(A!Qd8Fm!=h}0CZkBNU(gY;8PD?F-m(-Pm zpDl09@87Qku0RF-{r0ii1=byo1>E3ZBMQk&?dPtfw*Qj~ODo-8;>&@6((3|smf9#mM!nAz}~E3OhfTer6rRd<(V zpj-Vka(I|ooAE89?CQ3*V4~Ww@2ib#{Qn+dZf97#hPnAs^*r_aL}dN)kj-hDe@{Ys z&9r99ISrkq&@9;h-GlsK2bN3PfRDc33O9H{ei$e2fT&q-4yiRYebma>UqTwVKH`+v zAw1{J{igTge?o|EJFE0AfB)-P60fTKNil<+68wjHS+Su*?ycGJU;AH6n{L?aF^IAQ zQbc&pEg)r&+fHd^o*^}@X>KCledO}cYzU*>36tDLrKE2*4U+;eE;sr~P0pt%TPdj* z9W|6fY1vr!wg!ui!!dmT0GdN?TS3}ILK|=F9)mjLPH!=L4`LU^);~}fg*X}Z_W`Hw ztj}O)7ZL>|iC#@=XiIx!-_Ee26>~#wGl*uM0j?en4OnlpF+DPXz8_ z{JxZIbtQ3p;6s4h_V`pL!abRDZOG}wo_HnvMgcH2aBoJ!Q&i9=d%=N=i!yQ=8kLkj z-~KjkU18}EwD)oW754K2pV|g1koztZ4Mwbjfa1f~k-P{H0OfF)WL`Ve3hNpP77VHI z{~e8o6mF4Zhw?NWg7XYrTvDHAQ{TJJP;=yFE{I2njkYs%0^*Ur|C=&Wp`uiIw?z%O zwXQa1BzmsaQ=tN1KQxEf)*GjX1vJiv?0vx>9ocw$6Ph=gJgJ)qaGJ!W*TCfXw|)`1 zunu+cV@vL%J!$V%Ab*Uu_%R;sRNEl7;vAwd_hP(PGW`KH1MWKoTZkp)nb%sO| z+#}Kp_>&8Bx`}BP4g_xAe8`_Ht7)@hj;LH)+ct;p?gP88ObHE0K;Jn6N}Sd; zc(G;Qw6U_%l=Vx1BpH>*wL4ga_x00^$Jv1G#(_B~gyn^Z$X!&*}_plSYUKcn3HDjnScbKW)6So2$5(mHFSAb*7c0=E@_h0Kl$LhM z6H8`Nmj}+(*Qc^FFtX5t2w{6)US9rXc(LQJI@t(i09d|_B4OV=^n@_Dh@RRH%;)S6v1Cs5S|EP= z1|)jBtovuv#ycz8WL49538rh%J$)b7?^|uaw*+P4`&HF>xcCJKwDW)t59cS`a?LC( zB@0JqPtjx=q4h~ z`4&(d2%Iv5nJ9b(^m~ShEhuVtN!14I;3O8*h2YM*4;vki$x&V54;SI&`Bc-{$u(8k zCM`CG!M#{0Pxb~+HH36?pH^hPLcr>@*HoKbY^>8Ggbes((7OhT1gjt~$Df-n=tvN7 zSZ5aiO?xsW>2*IMxwf0)njdZLuJ8Ccwgf23{E@spL36g)*wv^p+3so?mVCW;;Bqo0 zr0L_0fOxVf=s04YRx~#*uI6YF*(dr>7a~n`Cr?7*wwl@=9q|q*_;OT11U-CtFe<11 zH-g}8Ll`a8huUQ63dx%Ef>kU`f~aUa*|cV$?sT#c&7AZjaIcSd3D&ARYzfrH6hyF& zA%-&7ZI2tt*Fg!1|CZNIYdNTXtOegP^j`D$fc$J$=UPi&x7T6BO91nkJa31*r@L!0 zuwh~s#YY1MJ2D|$zjnRfPWl}4;FCW&mk00dP;%aykf$m{LyS#}$tnX~{(1)a$=|#K zyzDSv5SKtT00^!moz`UxCTfqn>@QMh(zDb{+&?9NmX?9E9V6W!9Ka&`^31_1 zAz9ZwO~pI9lHou;qi6hbPkBiyri^O)R8VQyUYZM*F@qZNr%pP@%~;t?Id0*yjfMOK z+0;J*^HcaC&z$x3^oa1e>Z4n8rL7ICPk%L}kLuM5#*O0i*IdSodf;hKf~1RdQ<6=^ zMplKYmvlr&er_p@i=^-zm8e<5%I53VJjMLX2$xe#GLjRlS4y1|^rad$69WnFG~@l7 zwyz59E!Tum03rSSlGy)tJ^O#qI`z9$wKV+%*%9snb$aFkJV5d{ z$p8O@xMqcRyARw61bUveuz8-z*W%i>@5X)odW)tYIuT~D6ElYhhrwG; zIu2pYaK$gqbRuDm>BD-a0e8cg|#CImt#fd3;S z0Z%@vyqN_bm#pPfYEeU^Sy5kZEnzxZGJl5e3-Ju@?%!;L*(m$lm;mlOd@Zc`(dBG*rD{{Nv4RkA|W`z z|NQ)aOMw_v1P%fT9xI-V>HGY-UjofD8SO1m-Xo?dJ;?JS({$Q!EasVXx0Qjp9b@4{ z9IBh=MO={vKf9LW;g|yxujT2<@P0(Z;rCmPj<6J8s$xyy2LC^k<;}>AI140C_xkCf zZ0UX7_K7pWFpk6*Ij;uo@)LPD8m8$Idk%a2a@cqs#Z4N2YNxOb_TDSf6U?{Soa#<2 zxwwE(@m-;1oxc`QWuM=%?PY0>^Nx&k|5TM%X2A%vyH`+VL6P7Pg>-;b8FQ(zHgMd8 zHR7TmzgG z6gsBhxV(gV3KAq6AO)oO#i8RWqg#1|nix8rV|-xYKn+@0IegK*YR)dwSuqpp?X4x& zKtYToyh3ZnQfZIb4fK*)#ORuM(qC9~yT8u3e7Rr6kk2-%?D)RONOOa`?WQg!JJ90E zJmGq_$Hs9DsvOHbo5`EA`AU)aL_i?Lnd2li{}(S<_ErPMB@Xf{v~$*#wj(X}Q^zZe z<~~pDR-lxcXibCuN}-dso~627<4}Sd^l!=k$fkO(dpipbd1o3@d|-|MBJ+9Qm2V4y?%I^w1!J@t42?OJdfF?T5(=l`Z? z$ZJEUh)P+(-lFe)@J+4!dJ4jjNHALH-RV!-&Laxb48Rx!+I+-Fjms`CslHf&UZ%Rm zeDs7z81S;`*I%nV4;BY9WVna!v9a14wcNLpBna;Q>JlOXIr9!;?p(`WlgMX^>)r-# z^*Fvj^DR1i!M?%4>EBQ?Dh_a$2;Ji2OdF})^2F5I0YZ-V&XHrEPET#KYAFaWy?vpe zOq{1PdH&sEuMz2hc^P;~Q%{k0WpwMgMy=yoe#v);oLL7ktXMUzqN2!7#@>>&9<1@e z-n2+VdU04GJWsneig2Cs3N1(RKn5vVfWI`ACq=~Anjr26X{O4_@1>@??{F$JQ;k|y zZl+;xv$LX|zc}6FL>%9vxT7Ae_4LL@?lfakf&+m0IJH7uqO@-9vv~RIL5S+nUw>1M zjzrLaT>N6#Sah%Z#MDy26iEGfCH=SPW)ZtiL1z)-f+%QZN zLqzXdE-UxAXAmp9Chlz55);?4G*A$p-UbIRO5*nw6)uaYi=*%W3vj0A>Vgpxw`oyV%3WifXGFWQUQI3jNG3lwi!uImdF6yW$E z9I!&^#OlFb--;=)pl@F->&qX)o>y-wkjE&eo}l~_aa64VuR4e?XDfuujGMem7A$B6 z)6RQu9hHd@6Khq2>nnp}!o;L(X>lAC7FSLyo*928H7t z#DhTP1n+f#1yMSNJHB9>?oa;3adF{bpL<4gKU+T*ElIJl+Wwj!)|U~VqY-@ca0FF6 ztE6d1XmfRi8v zuh2dzQBCt#>OqC$ZbJ^nWBFektVmzwx@SZsVc&ODA_Dm(dv|$P<{dSS!29&7>SWwi z$U*Yj8kAs|`Yk!z*k|Jlinxio0RpoYxYHOa;05MRj6axMKv%*42>a*M%~+%ceVY(dc|{bw5j zO_-vh?*52T=p!Gnh(%H3CO8pUQsNRKQW*%WaL4^tf`Sg^#?tHD=vvkrjuTg+Tm14QY#~>548)^3PXfQHtJ~W%-?{dq~HScLK#<)eCRq>6>z_h zM2R6l-#bFgTh34375IjfD1<*P^re^d_$N1y?(s+^qWk}YH>3<$vbirK6i;Hyv7&zH zD4YyQdWAM*prSerP^sE7%$$cyQY;~~Ma6e-PvGr8?Xq3-! zsqY zTLu>YtQg4`*|243?7RPHtU$}H)L;Boc)`}-(_gKiFs=2xX{;?e=QYrc(Hz1SCk*?9t>PvEiW6^nJ*}|62`{IRsGavWUHSBe;%CKR#Mt|J+SA{` zmK{sHmW(_q>Uojts~aSR}aW~YoEo(`zj6!rhTmht71fmHje%#vnQ zi;1^{t5w2RT0kOUu>0NV7Vq{~Leub-n>&qvSY{ZdR4mlfs=1>(x1%!oWW1^sJ_^G?v2jee{d&JKe_e&b@|(s%cl`omb}G zP>zxhPG{SfFn`gH$mY$q>|kh!3LI^3V3~hEUTLD^4dkgV>Jt$~7dl=&Vu;^dKb zUd@&2f+(*J_ph(>@cRMb}51uj|s21gd>!6M``p1Wd) zEez1W>CW>@r0!2B#U9{RbD-FL1M1Pm^hI_@THlTeSD?8V04+HX)<&;iW3k|9?o5j< z?$}?NB6-mxb8_=i*&@~hQv60AS!}bg=;GWDl*XmL=dY{m&_&F)lYfabldHD^LZaJ< z=fAGKsC{7b90Lzw(aBLv+B_dgJf39FNz7nc*Ls9Dyw-9qcat_*ai$b6tc<0ypGu4; zm1e~JA2YOE{A1|%r?W2<0D3%_=B+clm&<${m%W(W-18rmHoRW)F_v@)R#E9?-_jb9 zXTY|vY>mdq?tQSBy?rW`)*(_t#`2cD?PX#$5hgP;VtrVSlr8eFBuh^@G)Ha zu}9aPYhx|51tsR|WgU?WUjkaRs$PEAYp-nt_?J}S#MXXLQ|MV)DIyU) zJ*d2jL8w}PswldXB_O8<<%Qgkzxj4DNoo~HJ++7cS$M-V<}1byQ&Mo!NjZZ2fGKpE zHViTr7x%0lkg=|9KKDt9_utZV4O({%Xe8Rs&Ed)IzGb zb4pQU<2}+Dj!)(+Afq`tD&Qj9|-c3KLDTDtP_2D}^{( zT6iS0T3mH}x~p((w|OJO#V@&w@>xXtE9iWP007e)nIns!u%|t2j_V-cvk}=T0N8I#F}h0in#sWae9EF&8RE2Kzo*dJPeTJX5uCO044I_{O;#;I_M)e8lXKg413S(tu_l9PL`hVgC|1%+lm)bd_$U*Jymdsk`G-5lPMhdI%w%aKs5ET%YzB>4L6zJkhe0EAH`&N z$)msV6podhQRPZY*hT(3b-T7yf@NT&(Yx2Z?Q$sKh7IMjzOMHLC*8(l zW%A^R({94VM4cxS{-~WI@0_nL-G0C>`FwPhn~MWA=Z%JRi|(GD(c&7Tl1}C(4GD>F8$y4FI(Nq!2Jazu)(E-IpoM0f8|fWvI$ZqfX`#gEIZf*}JU#uGNnd#+f_Rzhoufu?5O zkhoM)Hb(Vl6jOWbVS!aYwtj2I{=xRQ>p_|G2^2+^k>vmIPtQwI6sWDOy}3Q8tJ5wz zA4ChE`IJ`Wvroe1Z9j&}c0DvH4CRW{Zt|%jYS_e;27G3gR$A>XO?tSd7tPGI3hab? z-%v(+WmZHH9VrzBuo6K8z1hyorOyXv*!PE@ZawUE!8?9cDTjE&LgLZCkE>a&p`FP) z>5$Gxp}c-0?1q0sqz~7b7^Ja}EiJxE{Yj=1+6{BTUs^Hj;F9#|BLL}+spBst%~t7Q zVII)#pU7?kpYuA&2M(*BS~DraIuX)h5_)e)qK77`7=iBRTV4a6*nUs1XQ4qw2M{?U z5PSYxu$hmvmE(QQ%z5$vbOqX~y3y_CGEJCYYk*sv+StG7C3g0TmDOW(u& z5pWc>?|8VsmQhVPcqY84=f*@(@S%-3NJRLm{$-2ZWwtZ_t}M}Iv{J@xyL&L>Xq$Wk zk&eQZIxcWi;|3K`zaMWHBz)pjRC?I9v^FM0;TL~jrasvAxwUytXBt6729gAAt$~SV zMO8j)xt^mn7uO6MEpp?p!m*>HZz!)~{<_NRR9Lx89P>!;jKqnVmzROh0P%|dVe-(ks0*OBV3lf5&aOuY8(h-7>+<4^eN_M>~ieS6p& zm`EdY=AJ#UDKs@sh>3`pX4$*&!itI7>C1k_`5%lw-hX)L({y;Q2QIZF3YoqBaJDZFZ9TJ(7}^rpDhQDBF2s&rb{ZM8?A0w)ZyEAAe_O-870res5O{`{rz-H&g(y1%&y5IzRnr=l~_gE;|Py^j@95o}st zZpKCF9?Ix?4Vrv4>&i=XI-VBhP)-#GbjkG7v%>Vw{5sNYXl%8H6TfIh=B}hgr z$QKfX(YfM%I4$TtQ1^X*K6IDO+4pzXEC?Uf<}mw+!p^%Zc60wU%e$^)tx^+5VkJ4p=g*c-ved?J#n?ML^GyJcTnmi&*zXSw>>;Za%(ksH-H*za<9ET7cYP% zjOT$Mj=kI@3w%ck_rcUl|9fXILe_@5>ih#MQeR8l{7UlNXf1_{I*_~T{Dy|?+nD#A>k}5}VY!sU`$&u48hfXNYHs`OFNWS}`c*&Z~ zaaX7T=6j^aLi zFspR9o68eEd~SLnrpki2y)HzUY5ibtLazQ8MX=LoV@+)8zhvBW-jNT4IWCsR$oi2T zs439VeT|zMw`R(hojp^sURisVaS+7px7z-wG$me3kMdcq8drvw*YC3wkrn%CkCWeB zlkjfC|E(QcRbJ_tiaz>Y%E)IvRit&?IZ}$-l$GV2rF4?poHw4p&coQpfDMm5Tq&-( z$i*qiAW^6X4Q{Xg<3d~-P`)Xpn~;8dD5(QNvn}KXu%zxd$9PdUv{I1CVcTqGu8_DX z%5JP})_Gahz-deRdBuqz@uoQOgi7o=arWZo6DzhoXw48=TaWLz(HAj|Q6Kw7|+)}A$4DJ zPv*2Vr9x)-;|pdT7|h_eBbUsJ_5YwR*ji@+N_a5Pq53x4*cTX)`lF(hQ+tB?3pJ)IkHmE|e{T+QWviwy$gO0W~ zNi(L~cPk(_t@Bj%MKukv&omTnXE)V^!A6KgefU#aIaxF7gcg+x{e0iMcnr- zQH)B*$2n}ylH+vdAH5asNnr?Ld?L2#zsCJEy@2YVHv_&&Y^5z>$9*mCDLl=Z;j-`H z4Tx&A`Ot|_P2UT9XS7WlmLzS#UlT?}0m7b#y)>?xsfYcqReLjv&(GAR^JhzoiiYK> zP0_zQ^Ld#MiTaZg6M;@S$^hvr_MMjABz}c@ryp;Q*DKz})@1ZW088)?z*@>&g{D>9 z^T`%^l3A<5DiRLTX@}TD;HDH_XJ;8+X#eJPmkSnpd+Ip=sAeeh1azmRGuf1X=<7@B ze_q<8ng=Lf@ZsZTAl`c3-njw|rvgjM12Z{U(VdIOC-D`d4O?HHhb3vLNoa~#MT9rn zl%cWPTvm9GU2Uj58U&tS?=J$3=}Pi5uG}pi9)2BpTN*Z!H;C5JJHQBay6f^C%+9*@ z^ziJRlbWt&e-PvqbT*!&2dZ1XR9ao{%r#I-aDsLO#3c-f#qU9~%^6NbT(^ks;8my5 z)JL}H{IIy0{49Y{@Tcw3Gy(l=FNrtEXq8z+jl%9q>2q6K?|U1|AmDyOX_QZ`{!nU1 zM$n{DQ_ob=#2)SNzHuXgT3n%oomCVlWS5R`+MX2w3)H=d=BE|b0ZgzLfG&pr=yQAO z{*M2$T2{^rXJ5X`V7_UuOqQ9FcgptQBRGu-?52CoA8> z&C3f)n|M<;haKzX=9y}4hhOCgO5#sjjRo4U8L`_n=@0y;2iSL^Rpz9WEg>L~({c+1 zbr{uWtL!drD{3VPKk^Cu(WL>1h}ZcujNU^xqC4TUwJYvRI)@sSA=aS}-rDMg1k;0F zmFJrLps?Zvs4hO^QYO%_XGBlEku$(3<}fBIzl8xg0iz3g&9s{}AO2%^p#WPd_Tm2fNS(#Q%Crak1FJC;(7jhPK;T z9D!ei&=ShZUzTlma?gMigyU0v?}ErLmhjocxbVRbmHlmPObjv$a)`ZcKg_f`uVUlH zb!H%>KOATpRyM`c9*63f?025Wme{6%L{bTpnA|L)liDd(1h4}H@Q)Xq6KZuTTR;F< zA4T;ZpOZnxN4+m<3XnRh->f^N*U{Cj+w?KqL1cj}D^SDtB7_*VY|2|xoH5QGOn0c1 zn8)wLjVCQEXij^LnC^F^Wz}!K_7PzYmA>-;Y!SNUobWR3)0u{LrPk3AC^lSw|As-F ztSJ(B@i?q~r~MMB6Eu*22TE3cKKMD^qN7Ed&~~Ti@N@CANSpW^@WK^f#lY4gNe@(6UO*QKmpQ4 z&IaHqY^k>@TZd6c1yo)+>8ITWM+_Up;2q}*;d&(g{Z;njSd@K-tt%O(!=L*_-RtGM zfY0^%A%QW^K0GyCfhq|5IJ_>&448Ms2oY9U^^@R}vcL9cd@p$SHziX)a|#t-_r1gV z1l%|>(Rc$YFU$?13!ufv5Q~{^Oq?{)GGdM`rPl$n2Ac-|rNIt18jL(I%SIhaKL$nB zkWL%X*@lBYP?~{HzbCB(<_)9qco#i+F}yx*ic8Rnn#!M@t*m?g{hjJhT2ld!>6@ol z0mWiKmFg#5FXZ6qe?r{{bB$@5^)^Q{KXG?$hCT;3oi_WgnB^|`rA$19RqxL73<9e+7Hhqh!fAnaX$ISeg^I?Z~C{_#GAdd@!^os$Z2 z*EjtCY=M+B{BT;mZ0kMJbM{1YtNAb&B+5}>Fd;+h#qeg!Y#e8ywx;GXlkrlEGLu3vat{^M8Wc8~$~^pBT}npLapLyQ#aRW&q5S-!L9Nr8w5Mz=NxqXe zMjb8Bj%S=sq1f39?0^iUvby^Ees*&)W#3h$7!7Oumqaa3m-?V>wP*P)%(k;ODrmF= z@T~V&ZaK6KewCJow{UOf#I!Do;7Nx*KRkn=K=AAqE34sEi$@cG+E19~b6n)U{3Rhl zQkwH^d4@y*6z`aW!TLWAB>(*$c4YaKQ>S)-fYp6zAG=iK!bDhrM|%&x&Z!a(8Vaa{ zbBOWk%AJYm-^ebq4qt^$arVk$zX8slA(}*RP1&_^@cK<4|AGVJLabx$KTd8B0p++V zRq7tZv56omC}iX9HRoq!11`<+Gaqtf1xPLlq0&<&avV#?V7T+Wmin z^76mx>{B-*YX}vE`89k&wv9OUJ-3+BZ8#;8EiGkDWHGo4#49)O-9k@2K7jHyVU5|^ z@KiATHK|~zDk;j_<34udJ~7n z|1C13`BTePhx0*81KInrbkGGsTNf3y_jo>7*}&eqZUDkc*RgmOu?nn_W~Ojn1O9=I z3BI%oiloX*E9%|xm7{P9$Vo4sQIkWD3Q)uNg1B^HN(nnCrYla}4Q9mG(zlKT>jxla z7y7d2aUl1s4xUY+ZjQ9XKA6SNUM~y|rYFar>9b1^H5zLGD6`IJ-#=0!!ivzSZN1il zl_;ZUiizM6Hopnh1Zvgg)lyFiqvf7DzH@*x2lJoe_f>q6|Dn0xZY{oauQ??(4<-E9 zP0&3k>yU?(!oPgSKKUli?Smv7ow74n$EL{HlEpoPur;%6_-t4ooChfm*3dE0+)qfX zt{)(nqjfCd8}u50lLfXBs}-Bd7XDC!DR`q&WSy|6`997_x`QS@0&we3_-~=~@t<-a=K{H4* zts4_02HDouz9b>RwAiuw3}XN}y@(i7Jvyq8d7VM@msvzWnNAcWBVdMWP}0DF3Pa0h z+~F&+&%r+TZ}F#UzRVp0`~T_}IjH~Fy0!v22l3R6o}NbA(w=)5I0fr8{kY!F&TDHR z0H=UZ9O+)~N|K&s<8D7x#)qUZ4~KHh3-p&r7^_d$P_wneVB?y@Q)Xq@@VRjUJPrEcYvjXzYJ z>nI43w+lVZd6*WUBwzErP-L(iSbU~vA*RnyX?VM&0>Zrz&R|eBLJOeZ3wo8*!O;rb zSAW*XStcHJsW9QYrKYZ!Z+tj9s!RLVt5g#fqn#wsKh48<5sAzND|GiLKb2a05O#}$ z9Z)gj^pxJ7xBL~C{+b9l8Pa?&SEI<+Wahzv@?%Y42s90AY8i5Gllpw2Izg4u0R=jHBY?_0T$E+6ZJyV+|8Z@bPSXKioo_$*|LWZD= zPbkYe)Z(=J0dc-*!w!;y8>f6hvsUn<`QU^hl+Hzt#X1drl?!NUqQXgy?*dL6P`*p} zR?LQs2o5eV^}g#7VR5krC7;Xm7WrPdt}+y?4O{}vSlj9PErn(w>WI-+I34RNg^pCl zo!xvH0X1*MD+@(zNPx=R-3lD@am32FVjP8M2I>2E-u&OgGvbpEu?~Rkc(*zMQx9)x zMz0dU`T+$Wmk+C8iu=;UM&MMe4xp*3r*|+bDNSoDu(~6am&4ILgOn^Rv0CLou?6MC z6?O|rZeVg<+QG!1=9S>2T4i{W`o8Zpwb0N!8{dRpr2^$RWG=pcYe)DM z<``|%O=!Xq+mFU-s2{u>pnqJCZzu$2be+$>kg{A+TsbcuETY~OFGQJqQ%|bd|E%Z+ z<|QQgo)H8#tY2!G_EuVIeD=kf11zP4`p$-qvX6YTcjG5BY}&$*)g#bV_=l)Jb-$~- zuSn)6gKrs<+Gs_tzAT(m+Po-p!Gr3LJdHr>pv@TS@a0Y;Imd{WrZvVpru=+RStPQL z9H?sAItn5V&<0>eS+)8zU!h3YBTp2|JucE76!pu~6?PG~&d1pMvHA6Qf+c7l>;@s< zFVcS-cA@-J6E~cSBoxX+!w#waQw4J_Dw-l7QM6Z^k^!MzX=uA}YW2 zI8~#Il85f{R&@W^�Qo``6l+ajR7@$Av(1Kz}P{AiTUVS#Mdt(19_0Nq^Rg(Hz(1 zGsf9cf=rD|wv6nL)q$($y-PGzM|*YL8|fLHLGY(p=BCD36xnrs#L7Ah`ov*3H>raA-B31WX#EQJNjciZdRwe>vm0#nCh>{&mP3^p45ePQ~N4Qd$-Z z+im)My83ZzH;Z44)+saKw&cM75&X~3|4`unjRM_E7X-19)n3_sM>}8!r@Ml_yTwa) zOQ`v4OYi{^zAyZM|GpUieIacj5vYhb^u8GH{rk}S_g{QQh5a8B9Gxv}t-Sy56L>Ce zh=B=uKHB>3nlC+BU0yp|**aLVx_i4=vf8@1n?oR8>5Ds8$y{~s-Rn{B?Sn08LWtPO z1De^L>EU}>+r?>rVw#9tex Mp!THXv03o{0`DXv0ssI2 literal 0 HcmV?d00001 diff --git a/apps/web-ui/public/mstile-70x70.png b/apps/web-ui/public/mstile-70x70.png new file mode 100644 index 0000000000000000000000000000000000000000..320accce6799d6b764b47cb7837734bb53127cc4 GIT binary patch literal 4158 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 LjL~nkFc1F^iQlZ? literal 0 HcmV?d00001 diff --git a/apps/web-ui/public/safari-pinned-tab.svg b/apps/web-ui/public/safari-pinned-tab.svg new file mode 100644 index 00000000..b21d25be --- /dev/null +++ b/apps/web-ui/public/safari-pinned-tab.svg @@ -0,0 +1,33 @@ + + + + +Created by potrace 1.14, written by Peter Selinger 2001-2017 + + + + + diff --git a/apps/web-ui/public/site.webmanifest b/apps/web-ui/public/site.webmanifest new file mode 100644 index 00000000..fa99de77 --- /dev/null +++ b/apps/web-ui/public/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/apps/web-ui/remix.env.d.ts b/apps/web-ui/remix.env.d.ts new file mode 100644 index 00000000..dcf8c45e --- /dev/null +++ b/apps/web-ui/remix.env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/apps/web-ui/tailwind.config.cjs b/apps/web-ui/tailwind.config.cjs new file mode 100644 index 00000000..80e49b5f --- /dev/null +++ b/apps/web-ui/tailwind.config.cjs @@ -0,0 +1,36 @@ +const { createGlobPatternsForDependencies } = require('@nx/react/tailwind'); +const { join } = require('path'); +const defaultTheme = require('tailwindcss/defaultTheme'); +const { withTV } = require('tailwind-variants/transformer'); + +/** @type {import('tailwindcss').Config} */ +module.exports = withTV({ + content: [ + join( + __dirname, + '{src,pages,components,app}/**/*!(*.stories|*.spec).{ts,tsx,html}' + ), + ...createGlobPatternsForDependencies(__dirname), + ], + darkMode: 'selector', + theme: { + extend: { + fontFamily: { + sans: ['Inter var', ...defaultTheme.fontFamily.sans], + }, + fontSize: { + '2xs': '0.6875rem', + code: '0.8125rem', + }, + screens: { + '3xl': '1850px', + }, + }, + }, + plugins: [ + require('tailwindcss-react-aria-components'), + require('tailwindcss-animate'), + require('@tailwindcss/forms'), + require('@tailwindcss/container-queries'), + ], +}); diff --git a/apps/web-ui/test-setup.ts b/apps/web-ui/test-setup.ts new file mode 100644 index 00000000..85205829 --- /dev/null +++ b/apps/web-ui/test-setup.ts @@ -0,0 +1,3 @@ +import { installGlobals } from '@remix-run/node'; +import '@testing-library/jest-dom/matchers'; +installGlobals(); diff --git a/apps/web-ui/tests/routes/_index.spec.tsx b/apps/web-ui/tests/routes/_index.spec.tsx new file mode 100644 index 00000000..c2d95c75 --- /dev/null +++ b/apps/web-ui/tests/routes/_index.spec.tsx @@ -0,0 +1,21 @@ +import { createRemixStub } from '@remix-run/testing'; +import { render, screen, waitFor } from '@testing-library/react'; +import Index, { clientLoader } from '../../app/routes/_index'; + +test('renders loader data', async () => { + const RemixStub = createRemixStub([ + { + path: '/', + Component: Index, + loader: clientLoader, + }, + { + path: '/overview', + Component: () =>

Overview

, + }, + ]); + + render(); + + await waitFor(() => screen.findByText('Overview')); +}); diff --git a/apps/web-ui/tsconfig.app.json b/apps/web-ui/tsconfig.app.json new file mode 100644 index 00000000..fb96972f --- /dev/null +++ b/apps/web-ui/tsconfig.app.json @@ -0,0 +1,27 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": [ + "../../@types/vite.d.ts", + "../../@types/load-context.d.ts", + "../../@types/global-env.d.ts" + ] + }, + "include": [ + "remix.env.d.ts", + "app/**/*.ts", + "app/**/*.tsx", + "app/**/*.js", + "app/**/*.jsx" + ], + "exclude": [ + "tests/**/*.spec.ts", + "tests/**/*.test.ts", + "tests/**/*.spec.tsx", + "tests/**/*.test.tsx", + "tests/**/*.spec.js", + "tests/**/*.test.js", + "tests/**/*.spec.jsx", + "tests/**/*.test.jsx" + ] +} diff --git a/apps/web-ui/tsconfig.json b/apps/web-ui/tsconfig.json new file mode 100644 index 00000000..d95d0312 --- /dev/null +++ b/apps/web-ui/tsconfig.json @@ -0,0 +1,26 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2019"], + "isolatedModules": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "moduleResolution": "node", + "resolveJsonModule": true, + "target": "ES2019", + "strict": true, + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true + }, + "include": [], + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/apps/web-ui/tsconfig.spec.json b/apps/web-ui/tsconfig.spec.json new file mode 100644 index 00000000..9b53fa8b --- /dev/null +++ b/apps/web-ui/tsconfig.spec.json @@ -0,0 +1,30 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest", + "../../@types/global-env.d.ts" + ] + }, + "include": [ + "vite.config.ts", + "vitest.config.ts", + "app/**/*.ts", + "app/**/*.tsx", + "app/**/*.js", + "app/**/*.jsx", + "tests/**/*.spec.ts", + "tests/**/*.test.ts", + "tests/**/*.spec.tsx", + "tests/**/*.test.tsx", + "tests/**/*.spec.js", + "tests/**/*.test.js", + "tests/**/*.spec.jsx", + "tests/**/*.test.jsx" + ] +} diff --git a/apps/web-ui/vite.config.ts b/apps/web-ui/vite.config.ts new file mode 100644 index 00000000..d833bbce --- /dev/null +++ b/apps/web-ui/vite.config.ts @@ -0,0 +1,69 @@ +import { vitePlugin as remix } from '@remix-run/dev'; +import { defineConfig, loadEnv } from 'vite'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + +const BASE_URL = '/ui/'; +const ADMIN_BASE_URL = process.env['ADMIN_BASE_URL'] || ''; +const SERVER_HEADERS = { + 'Set-Cookie': `adminBaseUrl=${ADMIN_BASE_URL}; SameSite=Strict; Path=/`, +}; + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), ''); + + return { + base: BASE_URL, + root: __dirname, + cacheDir: '../../node_modules/.vite/apps/web-ui', + plugins: [ + !process.env.VITEST && + remix({ + ssr: false, + basename: BASE_URL, + future: { + v3_fetcherPersist: true, + v3_relativeSplatPath: true, + v3_throwAbortReason: true, + }, + }), + nxViteTsPaths(), + ], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + server: { + headers: SERVER_HEADERS, + hmr: { + protocol: 'ws', + port: 3001, + }, + }, + preview: { + headers: SERVER_HEADERS, + }, + define: { + 'globalThis.env': { + VERSION: env.VERSION ?? 'dev', + }, + }, + + test: { + setupFiles: ['test-setup.ts'], + globals: true, + cache: { + dir: '../../node_modules/.vitest', + }, + environment: 'jsdom', + include: ['./tests/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/apps/web-ui', + provider: 'v8', + }, + }, + }; +}); diff --git a/libs/data-access/admin-api-fixtures/.babelrc b/libs/data-access/admin-api-fixtures/.babelrc new file mode 100644 index 00000000..fd4cbcde --- /dev/null +++ b/libs/data-access/admin-api-fixtures/.babelrc @@ -0,0 +1,10 @@ +{ + "presets": [ + [ + "@nx/js/babel", + { + "useBuiltIns": "usage" + } + ] + ] +} diff --git a/libs/data-access/admin-api-fixtures/.eslintrc.json b/libs/data-access/admin-api-fixtures/.eslintrc.json new file mode 100644 index 00000000..3456be9b --- /dev/null +++ b/libs/data-access/admin-api-fixtures/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/data-access/admin-api-fixtures/README.md b/libs/data-access/admin-api-fixtures/README.md new file mode 100644 index 00000000..ad901aa0 --- /dev/null +++ b/libs/data-access/admin-api-fixtures/README.md @@ -0,0 +1,3 @@ +# data-access-admin-api-fixtures + +This library was generated with [Nx](https://nx.dev). diff --git a/libs/data-access/admin-api-fixtures/project.json b/libs/data-access/admin-api-fixtures/project.json new file mode 100644 index 00000000..cd995969 --- /dev/null +++ b/libs/data-access/admin-api-fixtures/project.json @@ -0,0 +1,9 @@ +{ + "name": "data-access-admin-api-fixtures", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/data-access/admin-api-fixtures/src", + "projectType": "library", + "tags": [], + "// targets": "to see all targets run: nx show project data-access-admin-api-fixtures --web", + "targets": {} +} diff --git a/libs/data-access/admin-api-fixtures/src/index.ts b/libs/data-access/admin-api-fixtures/src/index.ts new file mode 100644 index 00000000..32820fc8 --- /dev/null +++ b/libs/data-access/admin-api-fixtures/src/index.ts @@ -0,0 +1 @@ +export * from './lib/adminApiMockHandlers'; diff --git a/libs/data-access/admin-api-fixtures/src/lib/adminApiDb.ts b/libs/data-access/admin-api-fixtures/src/lib/adminApiDb.ts new file mode 100644 index 00000000..15008a73 --- /dev/null +++ b/libs/data-access/admin-api-fixtures/src/lib/adminApiDb.ts @@ -0,0 +1,52 @@ +import { factory, manyOf, oneOf, primaryKey } from '@mswjs/data'; +import { faker } from '@faker-js/faker'; + +faker.seed(Date.now()); + +export const adminApiDb = factory({ + handler: { + name: primaryKey(() => `${faker.hacker.noun()}`), + ty: () => + faker.helpers.arrayElement(['Exclusive', 'Shared', 'Workflow'] as const), + input_description: () => + 'one of ["none", "value of content-type \'application/json\'"]', + output_description: () => "value of content-type 'application/json'", + }, + service: { + name: primaryKey(() => `${faker.hacker.noun()}Service`), + handlers: manyOf('handler'), + deployment: oneOf('deployment'), + ty: () => + faker.helpers.arrayElement([ + 'Service', + 'VirtualObject', + 'Workflow', + ] as const), + revision: () => faker.number.int(), + idempotency_retention: () => '1Day', + workflow_completion_retention: () => '1Day', + public: () => faker.datatype.boolean(), + }, + deployment: { + id: primaryKey(() => `dp_${faker.string.nanoid(27)}`), + services: manyOf('service'), + }, +}); + +const isE2E = process.env['SCENARIO'] === 'E2E'; + +if (!isE2E) { + const services = Array(3) + .fill(null) + .map(() => adminApiDb.service.create()); + Array(30) + .fill(null) + .map(() => + adminApiDb.deployment.create({ + services: services.slice( + 0, + Math.floor(Math.random() * services.length + 1) + ), + }) + ); +} diff --git a/libs/data-access/admin-api-fixtures/src/lib/adminApiMockHandlers.ts b/libs/data-access/admin-api-fixtures/src/lib/adminApiMockHandlers.ts new file mode 100644 index 00000000..9086f0ad --- /dev/null +++ b/libs/data-access/admin-api-fixtures/src/lib/adminApiMockHandlers.ts @@ -0,0 +1,106 @@ +import * as adminApi from '@restate/data-access/admin-api/spec'; +import { http, HttpResponse } from 'msw'; +import { adminApiDb } from './adminApiDb'; +import { faker } from '@faker-js/faker'; + +type FormatParameterWithColon = + S extends `${infer A}{${infer P}}${infer B}` ? `${A}:${P}${B}` : S; +type GetPath = FormatParameterWithColon< + keyof Pick +>; + +const listDeploymentsHandler = http.get< + never, + never, + adminApi.operations['list_deployments']['responses']['200']['content']['application/json'], + GetPath<'/deployments'> +>('/deployments', async () => { + const deployments = adminApiDb.deployment.getAll(); + return HttpResponse.json({ + deployments: deployments.map((deployment) => ({ + id: deployment.id, + services: deployment.services, + uri: faker.internet.url(), + protocol_type: 'RequestResponse', + created_at: new Date().toISOString(), + http_version: 'HTTP/2.0', + min_protocol_version: 1, + max_protocol_version: 1, + })), + }); +}); + +const registerDeploymentHandler = http.post< + never, + adminApi.operations['create_deployment']['requestBody']['content']['application/json'], + adminApi.operations['create_deployment']['responses']['201']['content']['application/json'], + GetPath<'/deployments'> +>('/deployments', async ({ request }) => { + const requestBody = await request.json(); + const newDeployment = adminApiDb.deployment.create({}); + const services = Array(3) + .fill(null) + .map(() => adminApiDb.service.create({ deployment: newDeployment })); + + return HttpResponse.json({ + id: newDeployment.id, + services: services.map((service) => ({ + name: service.name, + deployment_id: service.deployment!.id, + public: service.public, + revision: service.revision, + ty: service.ty, + idempotency_retention: service.idempotency_retention, + workflow_completion_retention: service.idempotency_retention, + handlers: service.handlers.map((handler) => ({ + name: handler.name, + ty: handler.ty, + input_description: handler.input_description, + output_description: handler.output_description, + })), + })), + }); +}); + +const healthHandler = http.get< + never, + never, + adminApi.operations['health']['responses']['200']['content'], + GetPath<'/health'> +>('/health', async () => { + if (Math.random() < 0.5) { + return new HttpResponse(null, { status: 500 }); + } else { + return new HttpResponse(null, { status: 200 }); + } +}); + +const openApiHandler = http.get< + never, + never, + adminApi.operations['openapi_spec']['responses']['200']['content']['application/json'], + GetPath<'/openapi'> +>('/openapi', async () => { + return HttpResponse.json(adminApi.spec as any); +}); + +const versionHandler = http.get< + never, + never, + adminApi.operations['version']['responses']['200']['content']['application/json'], + GetPath<'/version'> +>('/version', async () => { + return HttpResponse.json({ + version: '1.1.1', + max_admin_api_version: 1, + min_admin_api_version: 1, + }); +}); + +export const adminApiMockHandlers = [ + listDeploymentsHandler, + healthHandler, + openApiHandler, + registerDeploymentHandler, + versionHandler, +]; diff --git a/libs/data-access/admin-api-fixtures/tsconfig.json b/libs/data-access/admin-api-fixtures/tsconfig.json new file mode 100644 index 00000000..9cc93474 --- /dev/null +++ b/libs/data-access/admin-api-fixtures/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "resolveJsonModule": true, + "esModuleInterop": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/libs/data-access/admin-api-fixtures/tsconfig.lib.json b/libs/data-access/admin-api-fixtures/tsconfig.lib.json new file mode 100644 index 00000000..8f9c818e --- /dev/null +++ b/libs/data-access/admin-api-fixtures/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/libs/data-access/admin-api/.babelrc b/libs/data-access/admin-api/.babelrc new file mode 100644 index 00000000..1ea870ea --- /dev/null +++ b/libs/data-access/admin-api/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/data-access/admin-api/.eslintrc.json b/libs/data-access/admin-api/.eslintrc.json new file mode 100644 index 00000000..75b85077 --- /dev/null +++ b/libs/data-access/admin-api/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nx/react", "../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/data-access/admin-api/README.md b/libs/data-access/admin-api/README.md new file mode 100644 index 00000000..899bf04f --- /dev/null +++ b/libs/data-access/admin-api/README.md @@ -0,0 +1,7 @@ +# data-access-admin-api + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test data-access-admin-api` to execute the unit tests via [Vitest](https://vitest.dev/). diff --git a/libs/data-access/admin-api/project.json b/libs/data-access/admin-api/project.json new file mode 100644 index 00000000..334c7165 --- /dev/null +++ b/libs/data-access/admin-api/project.json @@ -0,0 +1,19 @@ +{ + "name": "admin-api", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/data-access/admin-api/src", + "projectType": "library", + "tags": [], + "// targets": "to see all targets run: nx show project data-access-admin-api --web", + "targets": { + "create": { + "executor": "nx:run-commands", + "options": { + "commands": [ + "redocly lint ./libs/data-access/admin-api/src/lib/api/spec.json || true", + "openapi-typescript ./libs/data-access/admin-api/src/lib/api/spec.json -o ./libs/data-access/admin-api/src/lib/api/index.d.ts" + ] + } + } + } +} diff --git a/libs/data-access/admin-api/src/api.ts b/libs/data-access/admin-api/src/api.ts new file mode 100644 index 00000000..2cd7dc14 --- /dev/null +++ b/libs/data-access/admin-api/src/api.ts @@ -0,0 +1,4 @@ +import adminSpec from './lib/api/spec.json'; + +export type * from './lib/api'; +export const spec = adminSpec; diff --git a/libs/data-access/admin-api/src/index.ts b/libs/data-access/admin-api/src/index.ts new file mode 100644 index 00000000..3c9665d1 --- /dev/null +++ b/libs/data-access/admin-api/src/index.ts @@ -0,0 +1,4 @@ +export * from './lib/api/client'; +export * from './lib/AdminBaseUrlProvider'; +export * from './lib/api/hooks'; +export type * from './lib/api/type'; diff --git a/libs/data-access/admin-api/src/lib/AdminBaseUrlProvider.tsx b/libs/data-access/admin-api/src/lib/AdminBaseUrlProvider.tsx new file mode 100644 index 00000000..f8d00fc8 --- /dev/null +++ b/libs/data-access/admin-api/src/lib/AdminBaseUrlProvider.tsx @@ -0,0 +1,23 @@ +import { createContext, PropsWithChildren, useContext } from 'react'; + +const AdminBaseURLContext = createContext<{ baseUrl: string }>({ baseUrl: '' }); + +export function AdminBaseURLProvider({ + children, + baseUrl = '', +}: PropsWithChildren<{ baseUrl?: string }>) { + return ( + + {children} + + ); +} + +export function useAdminBaseUrl() { + const { baseUrl } = useContext(AdminBaseURLContext); + return baseUrl; +} diff --git a/libs/data-access/admin-api/src/lib/api/client.ts b/libs/data-access/admin-api/src/lib/api/client.ts new file mode 100644 index 00000000..802f72d5 --- /dev/null +++ b/libs/data-access/admin-api/src/lib/api/client.ts @@ -0,0 +1,236 @@ +/* eslint-disable @typescript-eslint/ban-types */ +import { UnauthorizedError } from '@restate/util/errors'; +import type { paths } from './index'; // generated by openapi-typescript +import { UseMutationOptions, UseQueryOptions } from '@tanstack/react-query'; +import type { FetchResponse, Middleware } from 'openapi-fetch'; +import createClient from 'openapi-fetch'; + +class RestateError extends Error { + constructor(message: string, public restate_code?: string) { + super(message); + } +} + +const client = createClient({}); +const errorMiddleware: Middleware = { + async onResponse({ response }) { + if (!response.ok) { + if (response.status === 401) { + // TODO: change import + throw new UnauthorizedError(); + } + const body: + | string + | { + message: string; + restate_code?: string | null; + } = response.headers.get('content-type')?.includes('json') + ? await response.clone().json() + : await response.clone().text(); + + if (typeof body === 'object') { + throw new RestateError(body.message, body.restate_code ?? ''); + } + throw new Error(body); + } + return response; + }, +}; + +client.use(errorMiddleware); + +export type SupportedMethods = keyof { + [PossibleMethod in keyof paths[Path] as paths[Path][PossibleMethod] extends NonNullable<{ + parameters: { + query?: unknown; + header?: unknown; + path?: unknown; + cookie?: unknown; + }; + requestBody?: + | { + content: { + 'application/json': unknown; + }; + } + | never; + }> + ? PossibleMethod + : never]: paths[Path]; +}; + +export type OperationParameters< + Path extends keyof paths, + Method extends SupportedMethods +> = paths[Path][Method] extends { + parameters: { + query?: unknown; + header?: unknown; + path?: unknown; + cookie?: unknown; + }; +} + ? paths[Path][Method]['parameters'] + : never; + +export type OperationBody< + Path extends keyof paths, + Method extends SupportedMethods +> = paths[Path][Method] extends { + requestBody: { + content: { + 'application/json': unknown; + }; + }; +} + ? paths[Path][Method]['requestBody']['content']['application/json'] + : never; + +export type QueryOptions< + Path extends keyof paths, + Method extends SupportedMethods +> = UseQueryOptions< + FetchResponse['data'], + FetchResponse['error'] +>; + +type QueryFn< + Path extends keyof paths, + Method extends SupportedMethods +> = Extract['queryFn'], Function>; + +type QueryKey< + Path extends keyof paths, + Method extends SupportedMethods +> = QueryOptions['queryKey']; + +export type MutationOptions< + Path extends keyof paths, + Method extends SupportedMethods, + Parameters extends OperationParameters, + Body extends OperationBody +> = UseMutationOptions< + FetchResponse['data'], + FetchResponse['error'], + { + parameters?: Parameters; + body?: Body; + } +>; + +type MutationFn< + Path extends keyof paths, + Method extends SupportedMethods, + Parameters extends OperationParameters, + Body extends OperationBody +> = Extract< + MutationOptions['mutationFn'], + Function +>; + +type MutationKey< + Path extends keyof paths, + Method extends SupportedMethods, + Parameters extends OperationParameters, + Body extends OperationBody +> = MutationOptions['mutationKey']; + +export function adminApi< + Path extends keyof paths, + Method extends SupportedMethods, + Parameters extends OperationParameters, + Body extends OperationBody +>( + type: 'query', + path: Path, + method: Method, + init: { + baseUrl: string; + parameters?: Parameters; + body?: Body; + } +): { + queryFn: QueryFn; + queryKey: QueryKey; +}; +export function adminApi< + Path extends keyof paths, + Method extends SupportedMethods, + Parameters extends OperationParameters, + Body extends OperationBody +>( + type: 'mutate', + path: Path, + method: Method, + init: { + baseUrl: string; + } +): { + mutationFn: MutationFn; + mutationKey: MutationKey; +}; +export function adminApi< + Path extends keyof paths, + Method extends SupportedMethods, + Parameters extends OperationParameters, + Body extends OperationBody +>( + type: 'query' | 'mutate', + path: Path, + method: Method, + init: { + baseUrl: string; + parameters?: Parameters; + body?: Body; + } +): + | { + queryFn: QueryFn; + queryKey: QueryKey; + } + | { + mutationFn: MutationFn; + mutationKey: MutationKey; + } { + const key = [path, { ...init, method }]; + + if (type === 'query') { + return { + queryKey: key, + queryFn: async ({ signal }: { signal: AbortSignal }) => { + const { data } = await (client as any)[String(method).toUpperCase()]( + path, + { + baseUrl: init.baseUrl, + signal, + headers: { + Accept: 'json', + }, + body: init.body, + params: init.parameters, + ...(path === '/health' && { parseAs: 'stream' }), + } + ); + return data; + }, + }; + } else { + return { + mutationKey: key, + mutationFn: async (variables: { + parameters?: Parameters; + body?: Body; + }) => { + const { data } = await (client as any)[String(method).toUpperCase()]( + path, + { + baseUrl: init.baseUrl, + body: variables.body, + params: variables.parameters, + } + ); + return data; + }, + }; + } +} diff --git a/libs/data-access/admin-api/src/lib/api/hooks.ts b/libs/data-access/admin-api/src/lib/api/hooks.ts new file mode 100644 index 00000000..03eb7673 --- /dev/null +++ b/libs/data-access/admin-api/src/lib/api/hooks.ts @@ -0,0 +1,79 @@ +/* eslint-disable @typescript-eslint/ban-types */ +import type { paths } from './index'; // generated by openapi-typescript +import { useMutation, useQuery } from '@tanstack/react-query'; +import { useAdminBaseUrl } from '../AdminBaseUrlProvider'; +import { + adminApi, + MutationOptions, + OperationBody, + OperationParameters, + QueryOptions, + SupportedMethods, +} from './client'; + +type HookQueryOptions< + Path extends keyof paths, + Method extends SupportedMethods +> = Omit, 'queryFn' | 'queryKey'>; + +type HookMutationOptions< + Path extends keyof paths, + Method extends SupportedMethods, + Parameters extends OperationParameters = OperationParameters< + Path, + Method + >, + Body extends OperationBody = OperationBody +> = Omit< + MutationOptions, + 'mutationFn' | 'mutationKey' +>; + +export function useListDeployments( + options?: HookQueryOptions<'/deployments', 'get'> +) { + const baseUrl = useAdminBaseUrl(); + + return useQuery({ + ...adminApi('query', '/deployments', 'get', { baseUrl }), + ...options, + }); +} + +export function useHealth(options?: HookQueryOptions<'/health', 'get'>) { + const baseUrl = useAdminBaseUrl(); + + return useQuery({ + ...adminApi('query', '/health', 'get', { baseUrl }), + ...options, + }); +} + +export function useOpenApi(options?: HookQueryOptions<'/openapi', 'get'>) { + const baseUrl = useAdminBaseUrl(); + + return useQuery({ + ...adminApi('query', '/openapi', 'get', { baseUrl }), + ...options, + }); +} + +export function useVersion(options?: HookQueryOptions<'/version', 'get'>) { + const baseUrl = useAdminBaseUrl(); + + return useQuery({ + ...adminApi('query', '/version', 'get', { baseUrl }), + ...options, + }); +} + +export function useRegisterDeployment( + options?: HookMutationOptions<'/deployments', 'post'> +) { + const baseUrl = useAdminBaseUrl(); + + return useMutation({ + ...adminApi('mutate', '/deployments', 'post', { baseUrl }), + ...options, + }); +} diff --git a/libs/data-access/admin-api/src/lib/api/index.d.ts b/libs/data-access/admin-api/src/lib/api/index.d.ts new file mode 100644 index 00000000..d76097d3 --- /dev/null +++ b/libs/data-access/admin-api/src/lib/api/index.d.ts @@ -0,0 +1,1643 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + '/services/{service}/state': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Modify a service state + * @description Modify service state + */ + post: operations['modify_service_state']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/services/{service}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get service + * @description Get a registered service. + */ + get: operations['get_service']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** + * Modify a service + * @description Modify a registered service. + */ + patch: operations['modify_service']; + trace?: never; + }; + '/subscriptions': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List subscriptions + * @description List all subscriptions. + */ + get: operations['list_subscriptions']; + put?: never; + /** + * Create subscription + * @description Create subscription. + */ + post: operations['create_subscription']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/invocations/{invocation_id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * Delete an invocation + * @description Delete the given invocation. By default, an invocation is terminated by gracefully cancelling it. This ensures virtual object state consistency. Alternatively, an invocation can be killed which does not guarantee consistency for virtual object instance state, in-flight invocations to other services, etc. A stored completed invocation can also be purged + */ + delete: operations['delete_invocation']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/subscriptions/{subscription}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get subscription + * @description Get subscription + */ + get: operations['get_subscription']; + put?: never; + post?: never; + /** + * Delete subscription + * @description Delete subscription. + */ + delete: operations['delete_subscription']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/version': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Admin version information + * @description Obtain admin version information. + */ + get: operations['version']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/services/{service}/handlers': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List service handlers + * @description List all the handlers of the given service. + */ + get: operations['list_service_handlers']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/services': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List services + * @description List all registered services. + */ + get: operations['list_services']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/deployments': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List deployments + * @description List all registered deployments. + */ + get: operations['list_deployments']; + put?: never; + /** + * Create deployment + * @description Create deployment. Restate will invoke the endpoint to gather additional information required for registration, such as the services exposed by the deployment. If the deployment is already registered, this method will fail unless `force` is set to `true`. + */ + post: operations['create_deployment']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/deployments/{deployment}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get deployment + * @description Get deployment metadata + */ + get: operations['get_deployment']; + put?: never; + post?: never; + /** + * Delete deployment + * @description Delete deployment. Currently it's supported to remove a deployment only using the force flag + */ + delete: operations['delete_deployment']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/health': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Health check + * @description Check REST API Health. + */ + get: operations['health']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/services/{service}/handlers/{handler}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get service handler + * @description Get the handler of a service + */ + get: operations['get_service_handler']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/openapi': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** OpenAPI specification */ + get: operations['openapi_spec']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + ModifyServiceStateRequest: { + /** + * Version + * @description If set, the latest version of the state is compared with this value and the operation will fail when the versions differ. + */ + version?: string | null; + /** + * Service key + * @description To what virtual object key to apply this change + */ + object_key: string; + /** + * New State + * @description The new state to replace the previous state with + */ + new_state: { + [key: string]: number[]; + }; + }; + /** + * Error description response + * @description Error details of the response + */ + ErrorDescriptionResponse: { + message: string; + /** + * Restate code + * @description Restate error code describing this error + */ + restate_code?: string | null; + }; + ServiceMetadata: { + /** + * Name + * @description Fully qualified name of the service + */ + name: string; + handlers: components['schemas']['HandlerMetadata'][]; + ty: components['schemas']['ServiceType']; + /** + * Deployment Id + * @description Deployment exposing the latest revision of the service. + */ + deployment_id: string; + /** + * Revision + * Format: uint32 + * @description Latest revision of the service. + */ + revision: number; + /** + * Public + * @description If true, the service can be invoked through the ingress. If false, the service can be invoked only from another Restate service. + */ + public: boolean; + /** + * Idempotency retention + * @description The retention duration of idempotent requests for this service. + */ + idempotency_retention: string; + /** + * Workflow completion retention + * @description The retention duration of workflows. Only available on workflow services. + */ + workflow_completion_retention?: string | null; + }; + HandlerMetadata: { + name: string; + ty: components['schemas']['HandlerMetadataType']; + input_description: string; + output_description: string; + }; + /** @enum {string} */ + HandlerMetadataType: 'Exclusive' | 'Shared' | 'Workflow'; + /** @enum {string} */ + ServiceType: 'Service' | 'VirtualObject' | 'Workflow'; + ListSubscriptionsResponse: { + subscriptions: components['schemas']['SubscriptionResponse'][]; + }; + SubscriptionResponse: { + id: components['schemas']['String']; + source: string; + sink: string; + options: { + [key: string]: string; + }; + }; + String: string; + ModifyServiceRequest: { + /** + * Public + * @description If true, the service can be invoked through the ingress. If false, the service can be invoked only from another Restate service. + */ + public?: boolean | null; + /** + * Idempotency retention + * @description Modify the retention of idempotent requests for this service. + * + * Can be configured using the [`humantime`](https://docs.rs/humantime/latest/humantime/fn.parse_duration.html) format or the ISO8601. + */ + idempotency_retention?: string | null; + /** + * Workflow completion retention + * @description Modify the retention of the workflow completion. This can be modified only for workflow services! + * + * Can be configured using the [`humantime`](https://docs.rs/humantime/latest/humantime/fn.parse_duration.html) format or the ISO8601. + */ + workflow_completion_retention?: string | null; + }; + /** @enum {string} */ + DeletionMode: 'Cancel' | 'Kill' | 'Purge'; + VersionInformation: { + /** + * Admin server version + * @description Version of the admin server + */ + version: string; + /** + * Min admin API version + * Format: uint16 + * @description Minimum supported admin API version by the admin server + */ + min_admin_api_version: number; + /** + * Max admin API version + * Format: uint16 + * @description Maximum supported admin API version by the admin server + */ + max_admin_api_version: number; + }; + ListServiceHandlersResponse: { + handlers: components['schemas']['HandlerMetadata'][]; + }; + ListServicesResponse: { + services: components['schemas']['ServiceMetadata'][]; + }; + CreateSubscriptionRequest: { + /** + * Source + * @description Source uri. Accepted forms: + * + * * `kafka:///`, e.g. `kafka://my-cluster/my-topic` + */ + source: string; + /** + * Sink + * @description Sink uri. Accepted forms: + * + * * `service:///`, e.g. `service://Counter/count` + */ + sink: string; + /** + * Options + * @description Additional options to apply to the subscription. + */ + options?: { + [key: string]: string; + } | null; + }; + RegisterDeploymentRequest: + | { + /** + * Uri + * @description Uri to use to discover/invoke the http deployment. + */ + uri: string; + /** + * Additional headers + * @description Additional headers added to the discover/invoke requests to the deployment. + */ + additional_headers?: { + [key: string]: string; + } | null; + /** + * Use http1.1 + * @description If `true`, discovery will be attempted using a client that defaults to HTTP1.1 instead of a prior-knowledge HTTP2 client. HTTP2 may still be used for TLS servers that advertise HTTP2 support via ALPN. HTTP1.1 deployments will only work in request-response mode. + * @default false + */ + use_http_11: boolean; + /** + * Force + * @description If `true`, it will override, if existing, any deployment using the same `uri`. Beware that this can lead in-flight invocations to an unrecoverable error state. + * + * By default, this is `true` but it might change in future to `false`. + * + * See the [versioning documentation](https://docs.restate.dev/operate/versioning) for more information. + * @default true + */ + force: boolean; + /** + * Dry-run mode + * @description If `true`, discovery will run but the deployment will not be registered. This is useful to see the impact of a new deployment before registering it. + * @default false + */ + dry_run: boolean; + } + | { + /** + * ARN + * @description ARN to use to discover/invoke the lambda deployment. + */ + arn: string; + /** + * Assume role ARN + * @description Optional ARN of a role to assume when invoking the addressed Lambda, to support role chaining + */ + assume_role_arn?: string | null; + /** + * Additional headers + * @description Additional headers added to the discover/invoke requests to the deployment. + */ + additional_headers?: { + [key: string]: string; + } | null; + /** + * Force + * @description If `true`, it will override, if existing, any deployment using the same `uri`. Beware that this can lead in-flight invocations to an unrecoverable error state. + * + * By default, this is `true` but it might change in future to `false`. + * + * See the [versioning documentation](https://docs.restate.dev/operate/versioning) for more information. + * @default true + */ + force: boolean; + /** + * Dry-run mode + * @description If `true`, discovery will run but the deployment will not be registered. This is useful to see the impact of a new deployment before registering it. + * @default false + */ + dry_run: boolean; + }; + RegisterDeploymentResponse: { + id: components['schemas']['String']; + services: components['schemas']['ServiceMetadata'][]; + }; + DetailedDeploymentResponse: { + id: components['schemas']['String']; + /** + * Services + * @description List of services exposed by this deployment. + */ + services: components['schemas']['ServiceMetadata'][]; + } & ( + | { + uri: string; + protocol_type: components['schemas']['ProtocolType']; + http_version: string; + additional_headers?: { + [key: string]: string; + }; + created_at: string; + /** Format: int32 */ + min_protocol_version: number; + /** Format: int32 */ + max_protocol_version: number; + } + | { + arn: components['schemas']['LambdaARN']; + assume_role_arn?: string | null; + additional_headers?: { + [key: string]: string; + }; + created_at: string; + /** Format: int32 */ + min_protocol_version: number; + /** Format: int32 */ + max_protocol_version: number; + } + ); + /** @enum {string} */ + ProtocolType: 'RequestResponse' | 'BidiStream'; + /** Format: arn */ + LambdaARN: string; + ListDeploymentsResponse: { + deployments: components['schemas']['DeploymentResponse'][]; + }; + DeploymentResponse: { + id: components['schemas']['String']; + /** + * Services + * @description List of services exposed by this deployment. + */ + services: components['schemas']['ServiceNameRevPair'][]; + } & ( + | { + uri: string; + protocol_type: components['schemas']['ProtocolType']; + http_version: string; + additional_headers?: { + [key: string]: string; + }; + created_at: string; + /** Format: int32 */ + min_protocol_version: number; + /** Format: int32 */ + max_protocol_version: number; + } + | { + arn: components['schemas']['LambdaARN']; + assume_role_arn?: string | null; + additional_headers?: { + [key: string]: string; + }; + created_at: string; + /** Format: int32 */ + min_protocol_version: number; + /** Format: int32 */ + max_protocol_version: number; + } + ); + ServiceNameRevPair: { + name: string; + /** Format: uint32 */ + revision: number; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + modify_service_state: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Fully qualified service name. */ + service: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['ModifyServiceStateRequest']; + }; + }; + responses: { + /** @description Accepted */ + 202: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 409: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 503: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + }; + }; + get_service: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Fully qualified service name. */ + service: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ServiceMetadata']; + }; + }; + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 409: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 503: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + }; + }; + modify_service: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Fully qualified service name. */ + service: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['ModifyServiceRequest']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ServiceMetadata']; + }; + }; + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 409: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 503: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + }; + }; + list_subscriptions: { + parameters: { + query?: { + /** @description Filter by the exact specified sink. */ + sink?: string; + /** @description Filter by the exact specified source. */ + source?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ListSubscriptionsResponse']; + }; + }; + }; + }; + create_subscription: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['CreateSubscriptionRequest']; + }; + }; + responses: { + /** @description Created */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['SubscriptionResponse']; + }; + }; + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 409: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 503: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + }; + }; + delete_invocation: { + parameters: { + query?: { + /** @description If cancel, it will gracefully terminate the invocation. If kill, it will terminate the invocation with a hard stop. If purge, it will only cleanup the response for completed invocations, and leave unaffected an in-flight invocation. */ + mode?: components['schemas']['DeletionMode']; + }; + header?: never; + path: { + /** @description Invocation identifier. */ + invocation_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Accepted */ + 202: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 409: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 503: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + }; + }; + get_subscription: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Subscription identifier */ + subscription: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['SubscriptionResponse']; + }; + }; + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 409: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 503: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + }; + }; + delete_subscription: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Subscription identifier */ + subscription: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Accepted */ + 202: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 409: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 503: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + }; + }; + version: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['VersionInformation']; + }; + }; + }; + }; + list_service_handlers: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Fully qualified service name. */ + service: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ListServiceHandlersResponse']; + }; + }; + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 409: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 503: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + }; + }; + list_services: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ListServicesResponse']; + }; + }; + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 409: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 503: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + }; + }; + list_deployments: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ListDeploymentsResponse']; + }; + }; + }; + }; + create_deployment: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['RegisterDeploymentRequest']; + }; + }; + responses: { + /** @description Created */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['RegisterDeploymentResponse']; + }; + }; + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 409: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 503: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + }; + }; + get_deployment: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Deployment identifier */ + deployment: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['DetailedDeploymentResponse']; + }; + }; + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 409: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 503: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + }; + }; + delete_deployment: { + parameters: { + query?: { + /** @description If true, the deployment will be forcefully deleted. This might break in-flight invocations, use with caution. */ + force?: boolean; + }; + header?: never; + path: { + /** @description Deployment identifier */ + deployment: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Accepted */ + 202: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 409: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + /** @description Not implemented. Only using the force flag is supported at the moment. */ + 501: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 503: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + }; + }; + health: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + get_service_handler: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Fully qualified service name. */ + service: string; + /** @description Handler name. */ + handler: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HandlerMetadata']; + }; + }; + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 409: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + 503: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorDescriptionResponse']; + }; + }; + }; + }; + openapi_spec: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + [key: string]: string; + }; + }; + }; + }; + }; +} diff --git a/libs/data-access/admin-api/src/lib/api/spec.json b/libs/data-access/admin-api/src/lib/api/spec.json new file mode 100644 index 00000000..12246e93 --- /dev/null +++ b/libs/data-access/admin-api/src/lib/api/spec.json @@ -0,0 +1,1898 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Admin API", + "version": "1.1.0" + }, + "paths": { + "/services/{service}/state": { + "post": { + "tags": ["service"], + "summary": "Modify a service state", + "description": "Modify service state", + "operationId": "modify_service_state", + "parameters": [ + { + "name": "service", + "in": "path", + "description": "Fully qualified service name.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModifyServiceStateRequest" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "Accepted" + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "409": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "503": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + } + } + } + }, + "/services/{service}": { + "get": { + "tags": ["service"], + "summary": "Get service", + "description": "Get a registered service.", + "operationId": "get_service", + "parameters": [ + { + "name": "service", + "in": "path", + "description": "Fully qualified service name.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceMetadata" + } + } + } + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "409": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "503": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + } + } + }, + "patch": { + "tags": ["service"], + "summary": "Modify a service", + "description": "Modify a registered service.", + "operationId": "modify_service", + "parameters": [ + { + "name": "service", + "in": "path", + "description": "Fully qualified service name.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModifyServiceRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceMetadata" + } + } + } + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "409": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "503": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + } + } + } + }, + "/subscriptions": { + "get": { + "tags": ["subscription"], + "summary": "List subscriptions", + "description": "List all subscriptions.", + "operationId": "list_subscriptions", + "parameters": [ + { + "name": "sink", + "in": "query", + "description": "Filter by the exact specified sink.", + "style": "simple", + "schema": { + "type": "string" + } + }, + { + "name": "source", + "in": "query", + "description": "Filter by the exact specified source.", + "style": "simple", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListSubscriptionsResponse" + } + } + } + } + } + }, + "post": { + "tags": ["subscription"], + "summary": "Create subscription", + "description": "Create subscription.", + "operationId": "create_subscription", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSubscriptionRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionResponse" + } + } + } + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "409": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "503": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + } + } + } + }, + "/invocations/{invocation_id}": { + "delete": { + "tags": ["invocation"], + "summary": "Delete an invocation", + "description": "Delete the given invocation. By default, an invocation is terminated by gracefully cancelling it. This ensures virtual object state consistency. Alternatively, an invocation can be killed which does not guarantee consistency for virtual object instance state, in-flight invocations to other services, etc. A stored completed invocation can also be purged", + "operationId": "delete_invocation", + "parameters": [ + { + "name": "invocation_id", + "in": "path", + "description": "Invocation identifier.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "mode", + "in": "query", + "description": "If cancel, it will gracefully terminate the invocation. If kill, it will terminate the invocation with a hard stop. If purge, it will only cleanup the response for completed invocations, and leave unaffected an in-flight invocation.", + "style": "simple", + "schema": { + "$ref": "#/components/schemas/DeletionMode" + } + } + ], + "responses": { + "202": { + "description": "Accepted" + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "409": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "503": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + } + } + } + }, + "/subscriptions/{subscription}": { + "get": { + "tags": ["subscription"], + "summary": "Get subscription", + "description": "Get subscription", + "operationId": "get_subscription", + "parameters": [ + { + "name": "subscription", + "in": "path", + "description": "Subscription identifier", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionResponse" + } + } + } + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "409": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "503": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + } + } + }, + "delete": { + "tags": ["subscription"], + "summary": "Delete subscription", + "description": "Delete subscription.", + "operationId": "delete_subscription", + "parameters": [ + { + "name": "subscription", + "in": "path", + "description": "Subscription identifier", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "202": { + "description": "Accepted" + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "409": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "503": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + } + } + } + }, + "/version": { + "get": { + "tags": ["version"], + "summary": "Admin version information", + "description": "Obtain admin version information.", + "operationId": "version", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VersionInformation" + } + } + } + } + } + } + }, + "/services/{service}/handlers": { + "get": { + "tags": ["service_handler"], + "summary": "List service handlers", + "description": "List all the handlers of the given service.", + "operationId": "list_service_handlers", + "parameters": [ + { + "name": "service", + "in": "path", + "description": "Fully qualified service name.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListServiceHandlersResponse" + } + } + } + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "409": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "503": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + } + } + } + }, + "/services": { + "get": { + "tags": ["service"], + "summary": "List services", + "description": "List all registered services.", + "operationId": "list_services", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListServicesResponse" + } + } + } + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "409": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "503": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + } + } + } + }, + "/deployments": { + "get": { + "tags": ["deployment"], + "summary": "List deployments", + "description": "List all registered deployments.", + "operationId": "list_deployments", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListDeploymentsResponse" + } + } + } + } + } + }, + "post": { + "tags": ["deployment"], + "summary": "Create deployment", + "description": "Create deployment. Restate will invoke the endpoint to gather additional information required for registration, such as the services exposed by the deployment. If the deployment is already registered, this method will fail unless `force` is set to `true`.", + "operationId": "create_deployment", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegisterDeploymentRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegisterDeploymentResponse" + } + } + } + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "409": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "503": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + } + } + } + }, + "/deployments/{deployment}": { + "get": { + "tags": ["deployment"], + "summary": "Get deployment", + "description": "Get deployment metadata", + "operationId": "get_deployment", + "parameters": [ + { + "name": "deployment", + "in": "path", + "description": "Deployment identifier", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DetailedDeploymentResponse" + } + } + } + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "409": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "503": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + } + } + }, + "delete": { + "tags": ["deployment"], + "summary": "Delete deployment", + "description": "Delete deployment. Currently it's supported to remove a deployment only using the force flag", + "operationId": "delete_deployment", + "parameters": [ + { + "name": "deployment", + "in": "path", + "description": "Deployment identifier", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "force", + "in": "query", + "description": "If true, the deployment will be forcefully deleted. This might break in-flight invocations, use with caution.", + "style": "simple", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "202": { + "description": "Accepted" + }, + "501": { + "description": "Not implemented. Only using the force flag is supported at the moment." + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "409": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "503": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + } + } + } + }, + "/health": { + "get": { + "tags": ["health"], + "summary": "Health check", + "description": "Check REST API Health.", + "operationId": "health", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/services/{service}/handlers/{handler}": { + "get": { + "tags": ["service_handler"], + "summary": "Get service handler", + "description": "Get the handler of a service", + "operationId": "get_service_handler", + "parameters": [ + { + "name": "service", + "in": "path", + "description": "Fully qualified service name.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "handler", + "in": "path", + "description": "Handler name.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HandlerMetadata" + } + } + } + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "409": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + }, + "503": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorDescriptionResponse" + } + } + } + } + } + } + }, + "/openapi": { + "get": { + "tags": ["openapi"], + "summary": "OpenAPI specification", + "externalDocs": { + "url": "https://swagger.io/specification/" + }, + "operationId": "openapi_spec", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "ModifyServiceStateRequest": { + "type": "object", + "required": ["new_state", "object_key"], + "properties": { + "version": { + "title": "Version", + "description": "If set, the latest version of the state is compared with this value and the operation will fail when the versions differ.", + "type": "string", + "nullable": true + }, + "object_key": { + "title": "Service key", + "description": "To what virtual object key to apply this change", + "type": "string" + }, + "new_state": { + "title": "New State", + "description": "The new state to replace the previous state with", + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + } + } + } + }, + "ErrorDescriptionResponse": { + "title": "Error description response", + "description": "Error details of the response", + "type": "object", + "required": ["message"], + "properties": { + "message": { + "type": "string" + }, + "restate_code": { + "title": "Restate code", + "description": "Restate error code describing this error", + "type": "string", + "nullable": true + } + } + }, + "ServiceMetadata": { + "type": "object", + "required": [ + "deployment_id", + "handlers", + "idempotency_retention", + "name", + "public", + "revision", + "ty" + ], + "properties": { + "name": { + "title": "Name", + "description": "Fully qualified name of the service", + "type": "string" + }, + "handlers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HandlerMetadata" + } + }, + "ty": { + "$ref": "#/components/schemas/ServiceType" + }, + "deployment_id": { + "title": "Deployment Id", + "description": "Deployment exposing the latest revision of the service.", + "type": "string" + }, + "revision": { + "title": "Revision", + "description": "Latest revision of the service.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "public": { + "title": "Public", + "description": "If true, the service can be invoked through the ingress. If false, the service can be invoked only from another Restate service.", + "type": "boolean" + }, + "idempotency_retention": { + "title": "Idempotency retention", + "description": "The retention duration of idempotent requests for this service.", + "type": "string" + }, + "workflow_completion_retention": { + "title": "Workflow completion retention", + "description": "The retention duration of workflows. Only available on workflow services.", + "type": "string", + "nullable": true + } + } + }, + "HandlerMetadata": { + "type": "object", + "required": ["input_description", "name", "output_description", "ty"], + "properties": { + "name": { + "type": "string" + }, + "ty": { + "$ref": "#/components/schemas/HandlerMetadataType" + }, + "input_description": { + "type": "string" + }, + "output_description": { + "type": "string" + } + } + }, + "HandlerMetadataType": { + "type": "string", + "enum": ["Exclusive", "Shared", "Workflow"] + }, + "ServiceType": { + "type": "string", + "enum": ["Service", "VirtualObject", "Workflow"] + }, + "ListSubscriptionsResponse": { + "type": "object", + "required": ["subscriptions"], + "properties": { + "subscriptions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SubscriptionResponse" + } + } + } + }, + "SubscriptionResponse": { + "type": "object", + "required": ["id", "options", "sink", "source"], + "properties": { + "id": { + "$ref": "#/components/schemas/String" + }, + "source": { + "type": "string" + }, + "sink": { + "type": "string" + }, + "options": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "String": { + "type": "string" + }, + "ModifyServiceRequest": { + "type": "object", + "properties": { + "public": { + "title": "Public", + "description": "If true, the service can be invoked through the ingress. If false, the service can be invoked only from another Restate service.", + "type": "boolean", + "nullable": true + }, + "idempotency_retention": { + "title": "Idempotency retention", + "description": "Modify the retention of idempotent requests for this service.\n\nCan be configured using the [`humantime`](https://docs.rs/humantime/latest/humantime/fn.parse_duration.html) format or the ISO8601.", + "type": "string", + "nullable": true + }, + "workflow_completion_retention": { + "title": "Workflow completion retention", + "description": "Modify the retention of the workflow completion. This can be modified only for workflow services!\n\nCan be configured using the [`humantime`](https://docs.rs/humantime/latest/humantime/fn.parse_duration.html) format or the ISO8601.", + "type": "string", + "nullable": true + } + } + }, + "DeletionMode": { + "type": "string", + "enum": ["Cancel", "Kill", "Purge"] + }, + "VersionInformation": { + "type": "object", + "required": [ + "max_admin_api_version", + "min_admin_api_version", + "version" + ], + "properties": { + "version": { + "title": "Admin server version", + "description": "Version of the admin server", + "type": "string" + }, + "min_admin_api_version": { + "title": "Min admin API version", + "description": "Minimum supported admin API version by the admin server", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + }, + "max_admin_api_version": { + "title": "Max admin API version", + "description": "Maximum supported admin API version by the admin server", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + } + } + }, + "ListServiceHandlersResponse": { + "type": "object", + "required": ["handlers"], + "properties": { + "handlers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HandlerMetadata" + } + } + } + }, + "ListServicesResponse": { + "type": "object", + "required": ["services"], + "properties": { + "services": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ServiceMetadata" + } + } + } + }, + "CreateSubscriptionRequest": { + "type": "object", + "required": ["sink", "source"], + "properties": { + "source": { + "title": "Source", + "description": "Source uri. Accepted forms:\n\n* `kafka:///`, e.g. `kafka://my-cluster/my-topic`", + "type": "string" + }, + "sink": { + "title": "Sink", + "description": "Sink uri. Accepted forms:\n\n* `service:///`, e.g. `service://Counter/count`", + "type": "string" + }, + "options": { + "title": "Options", + "description": "Additional options to apply to the subscription.", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "nullable": true + } + } + }, + "RegisterDeploymentRequest": { + "anyOf": [ + { + "type": "object", + "required": ["uri"], + "properties": { + "uri": { + "title": "Uri", + "description": "Uri to use to discover/invoke the http deployment.", + "type": "string" + }, + "additional_headers": { + "title": "Additional headers", + "description": "Additional headers added to the discover/invoke requests to the deployment.", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "nullable": true + }, + "use_http_11": { + "title": "Use http1.1", + "description": "If `true`, discovery will be attempted using a client that defaults to HTTP1.1 instead of a prior-knowledge HTTP2 client. HTTP2 may still be used for TLS servers that advertise HTTP2 support via ALPN. HTTP1.1 deployments will only work in request-response mode.", + "default": false, + "type": "boolean" + }, + "force": { + "title": "Force", + "description": "If `true`, it will override, if existing, any deployment using the same `uri`. Beware that this can lead in-flight invocations to an unrecoverable error state.\n\nBy default, this is `true` but it might change in future to `false`.\n\nSee the [versioning documentation](https://docs.restate.dev/operate/versioning) for more information.", + "default": true, + "type": "boolean" + }, + "dry_run": { + "title": "Dry-run mode", + "description": "If `true`, discovery will run but the deployment will not be registered. This is useful to see the impact of a new deployment before registering it.", + "default": false, + "type": "boolean" + } + } + }, + { + "type": "object", + "required": ["arn"], + "properties": { + "arn": { + "title": "ARN", + "description": "ARN to use to discover/invoke the lambda deployment.", + "type": "string" + }, + "assume_role_arn": { + "title": "Assume role ARN", + "description": "Optional ARN of a role to assume when invoking the addressed Lambda, to support role chaining", + "type": "string", + "nullable": true + }, + "additional_headers": { + "title": "Additional headers", + "description": "Additional headers added to the discover/invoke requests to the deployment.", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "nullable": true + }, + "force": { + "title": "Force", + "description": "If `true`, it will override, if existing, any deployment using the same `uri`. Beware that this can lead in-flight invocations to an unrecoverable error state.\n\nBy default, this is `true` but it might change in future to `false`.\n\nSee the [versioning documentation](https://docs.restate.dev/operate/versioning) for more information.", + "default": true, + "type": "boolean" + }, + "dry_run": { + "title": "Dry-run mode", + "description": "If `true`, discovery will run but the deployment will not be registered. This is useful to see the impact of a new deployment before registering it.", + "default": false, + "type": "boolean" + } + } + } + ] + }, + "RegisterDeploymentResponse": { + "type": "object", + "required": ["id", "services"], + "properties": { + "id": { + "$ref": "#/components/schemas/String" + }, + "services": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ServiceMetadata" + } + } + } + }, + "DetailedDeploymentResponse": { + "type": "object", + "anyOf": [ + { + "type": "object", + "required": [ + "created_at", + "http_version", + "max_protocol_version", + "min_protocol_version", + "protocol_type", + "uri" + ], + "properties": { + "uri": { + "type": "string" + }, + "protocol_type": { + "$ref": "#/components/schemas/ProtocolType" + }, + "http_version": { + "type": "string" + }, + "additional_headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "created_at": { + "type": "string" + }, + "min_protocol_version": { + "type": "integer", + "format": "int32" + }, + "max_protocol_version": { + "type": "integer", + "format": "int32" + } + } + }, + { + "type": "object", + "required": [ + "arn", + "created_at", + "max_protocol_version", + "min_protocol_version" + ], + "properties": { + "arn": { + "$ref": "#/components/schemas/LambdaARN" + }, + "assume_role_arn": { + "type": "string", + "nullable": true + }, + "additional_headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "created_at": { + "type": "string" + }, + "min_protocol_version": { + "type": "integer", + "format": "int32" + }, + "max_protocol_version": { + "type": "integer", + "format": "int32" + } + } + } + ], + "required": ["id", "services"], + "properties": { + "id": { + "$ref": "#/components/schemas/String" + }, + "services": { + "title": "Services", + "description": "List of services exposed by this deployment.", + "type": "array", + "items": { + "$ref": "#/components/schemas/ServiceMetadata" + } + } + } + }, + "ProtocolType": { + "type": "string", + "enum": ["RequestResponse", "BidiStream"] + }, + "LambdaARN": { + "type": "string", + "format": "arn" + }, + "ListDeploymentsResponse": { + "type": "object", + "required": ["deployments"], + "properties": { + "deployments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DeploymentResponse" + } + } + } + }, + "DeploymentResponse": { + "type": "object", + "anyOf": [ + { + "type": "object", + "required": [ + "created_at", + "http_version", + "max_protocol_version", + "min_protocol_version", + "protocol_type", + "uri" + ], + "properties": { + "uri": { + "type": "string" + }, + "protocol_type": { + "$ref": "#/components/schemas/ProtocolType" + }, + "http_version": { + "type": "string" + }, + "additional_headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "created_at": { + "type": "string" + }, + "min_protocol_version": { + "type": "integer", + "format": "int32" + }, + "max_protocol_version": { + "type": "integer", + "format": "int32" + } + } + }, + { + "type": "object", + "required": [ + "arn", + "created_at", + "max_protocol_version", + "min_protocol_version" + ], + "properties": { + "arn": { + "$ref": "#/components/schemas/LambdaARN" + }, + "assume_role_arn": { + "type": "string", + "nullable": true + }, + "additional_headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "created_at": { + "type": "string" + }, + "min_protocol_version": { + "type": "integer", + "format": "int32" + }, + "max_protocol_version": { + "type": "integer", + "format": "int32" + } + } + } + ], + "required": ["id", "services"], + "properties": { + "id": { + "$ref": "#/components/schemas/String" + }, + "services": { + "title": "Services", + "description": "List of services exposed by this deployment.", + "type": "array", + "items": { + "$ref": "#/components/schemas/ServiceNameRevPair" + } + } + } + }, + "ServiceNameRevPair": { + "type": "object", + "required": ["name", "revision"], + "properties": { + "name": { + "type": "string" + }, + "revision": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + } + } + } + } +} diff --git a/libs/data-access/admin-api/src/lib/api/type.ts b/libs/data-access/admin-api/src/lib/api/type.ts new file mode 100644 index 00000000..65d8f518 --- /dev/null +++ b/libs/data-access/admin-api/src/lib/api/type.ts @@ -0,0 +1,6 @@ +import type { components } from './index'; // generated by openapi-typescript + +export type Deployment = + components['schemas']['ListDeploymentsResponse']['deployments'][number]; +export type DetailedDeployment = + components['schemas']['DetailedDeploymentResponse']; diff --git a/libs/data-access/admin-api/tsconfig.json b/libs/data-access/admin-api/tsconfig.json new file mode 100644 index 00000000..50b36f3d --- /dev/null +++ b/libs/data-access/admin-api/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "resolveJsonModule": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json" +} diff --git a/libs/data-access/admin-api/tsconfig.lib.json b/libs/data-access/admin-api/tsconfig.lib.json new file mode 100644 index 00000000..eb8cfbfc --- /dev/null +++ b/libs/data-access/admin-api/tsconfig.lib.json @@ -0,0 +1,23 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": [ + "node", + "../../../@types/global-env.d.ts", + "@nx/react/typings/cssmodule.d.ts", + "@nx/react/typings/image.d.ts" + ] + }, + "exclude": [ + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx" + ], + "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] +} diff --git a/libs/data-access/admin-api/tsconfig.spec.json b/libs/data-access/admin-api/tsconfig.spec.json new file mode 100644 index 00000000..05a0e183 --- /dev/null +++ b/libs/data-access/admin-api/tsconfig.spec.json @@ -0,0 +1,26 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ] + }, + "include": [ + "vite.config.ts", + "vitest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] +} diff --git a/libs/data-access/admin-api/vite.config.ts b/libs/data-access/admin-api/vite.config.ts new file mode 100644 index 00000000..fec0b13b --- /dev/null +++ b/libs/data-access/admin-api/vite.config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + +export default defineConfig({ + root: __dirname, + cacheDir: '../../../node_modules/.vite/libs/data-access/admin-api', + + plugins: [react(), nxViteTsPaths()], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + test: { + watch: false, + globals: true, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../../coverage/libs/data-access/admin-api', + provider: 'v8', + }, + }, +}); diff --git a/libs/features/overview-route/.babelrc b/libs/features/overview-route/.babelrc new file mode 100644 index 00000000..1ea870ea --- /dev/null +++ b/libs/features/overview-route/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/features/overview-route/.eslintrc.json b/libs/features/overview-route/.eslintrc.json new file mode 100644 index 00000000..75b85077 --- /dev/null +++ b/libs/features/overview-route/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nx/react", "../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/features/overview-route/README.md b/libs/features/overview-route/README.md new file mode 100644 index 00000000..8ac97388 --- /dev/null +++ b/libs/features/overview-route/README.md @@ -0,0 +1,7 @@ +# features-overview-route + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test features-overview-route` to execute the unit tests via [Vitest](https://vitest.dev/). diff --git a/libs/features/overview-route/project.json b/libs/features/overview-route/project.json new file mode 100644 index 00000000..c20257ab --- /dev/null +++ b/libs/features/overview-route/project.json @@ -0,0 +1,9 @@ +{ + "name": "features-overview-route", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/features/overview-route/src", + "projectType": "library", + "tags": [], + "// targets": "to see all targets run: nx show project features-overview-route --web", + "targets": {} +} diff --git a/libs/features/overview-route/src/index.ts b/libs/features/overview-route/src/index.ts new file mode 100644 index 00000000..295eee53 --- /dev/null +++ b/libs/features/overview-route/src/index.ts @@ -0,0 +1 @@ +export * from './lib/overview.route'; diff --git a/libs/features/overview-route/src/lib/Deployment.tsx b/libs/features/overview-route/src/lib/Deployment.tsx new file mode 100644 index 00000000..fa69c890 --- /dev/null +++ b/libs/features/overview-route/src/lib/Deployment.tsx @@ -0,0 +1,61 @@ +import type { Deployment } from '@restate/data-access/admin-api'; +import { Icon, IconName } from '@restate/ui/icons'; +import { tv } from 'tailwind-variants'; +import { isHttpDeployment } from './types'; +import { Service } from './Service'; +import { Link } from '@restate/ui/link'; + +const styles = tv({ + base: 'w-full rounded-xl border bg-gray-200/50 shadow-[inset_0_1px_0px_0px_rgba(0,0,0,0.03)]', +}); + +function getDeploymentIdentifier(deployment: Deployment) { + if (isHttpDeployment(deployment)) { + return deployment.uri; + } else { + return deployment.arn; + } +} + +export function Deployment({ + deployment, + className, +}: { + deployment: Deployment; + className?: string; +}) { + return ( +
+ +
+
+
+ +
+
+
+ {getDeploymentIdentifier(deployment)} +
+
+ + {deployment.services.length > 0 && ( +
+
+ Services +
+ {deployment.services.map((service) => ( + + ))} +
+ )} +
+ ); +} diff --git a/libs/features/overview-route/src/lib/Details.tsx/Deployment.tsx b/libs/features/overview-route/src/lib/Details.tsx/Deployment.tsx new file mode 100644 index 00000000..03c5f9fa --- /dev/null +++ b/libs/features/overview-route/src/lib/Details.tsx/Deployment.tsx @@ -0,0 +1,29 @@ +import { Button } from '@restate/ui/button'; +import { + ComplementaryWithSearchParam, + ComplementaryClose, +} from '@restate/ui/layout'; + +export function DeploymentDetails() { + return ( + + + + + + + } + > + {DeploymentForm} + + ); +} + +function DeploymentForm({ paramValue }: { paramValue: string }) { + return
{paramValue}
; +} diff --git a/libs/features/overview-route/src/lib/Details.tsx/Service.tsx b/libs/features/overview-route/src/lib/Details.tsx/Service.tsx new file mode 100644 index 00000000..e4da914d --- /dev/null +++ b/libs/features/overview-route/src/lib/Details.tsx/Service.tsx @@ -0,0 +1,29 @@ +import { Button } from '@restate/ui/button'; +import { + ComplementaryWithSearchParam, + ComplementaryClose, +} from '@restate/ui/layout'; + +export function ServiceDetails() { + return ( + + + + + + + } + > + {ServiceForm} + + ); +} + +function ServiceForm({ paramValue }: { paramValue: string }) { + return
{paramValue}
; +} diff --git a/libs/features/overview-route/src/lib/RegisterDeployment/AdditionalHeaders.tsx b/libs/features/overview-route/src/lib/RegisterDeployment/AdditionalHeaders.tsx new file mode 100644 index 00000000..3421e84f --- /dev/null +++ b/libs/features/overview-route/src/lib/RegisterDeployment/AdditionalHeaders.tsx @@ -0,0 +1,97 @@ +import { Button } from '@restate/ui/button'; +import { + FormFieldGroup, + FormFieldLabel, + FormFieldInput, +} from '@restate/ui/form-field'; +import { IconName, Icon } from '@restate/ui/icons'; +import { useListData } from 'react-stately'; + +export function AdditionalHeaders() { + const list = useListData<{ key: string; value: string; index: number }>({ + initialItems: [{ key: '', value: '', index: 0 }], + getKey: (item) => item.index, + }); + + return ( + + + + Additional headers + + + Headers added to the discover/invoke requests to the deployment. + + + {list.items.map((item) => ( +
+ + list.update(item.index, { + ...item, + key, + }) + } + /> + : + + list.update(item.index, { + ...item, + value, + }) + } + /> + +
+ ))} + +
+ ); +} diff --git a/libs/features/overview-route/src/lib/RegisterDeployment/AssumeARNRole.tsx b/libs/features/overview-route/src/lib/RegisterDeployment/AssumeARNRole.tsx new file mode 100644 index 00000000..3b307642 --- /dev/null +++ b/libs/features/overview-route/src/lib/RegisterDeployment/AssumeARNRole.tsx @@ -0,0 +1,20 @@ +import { FormFieldInput } from '@restate/ui/form-field'; + +export function AssumeARNRole() { + return ( + + Assume role ARN + + Optional ARN of a role to assume when invoking the addressed Lambda, + to support role chaining + + + } + /> + ); +} diff --git a/libs/features/overview-route/src/lib/RegisterDeployment/Dialog.tsx b/libs/features/overview-route/src/lib/RegisterDeployment/Dialog.tsx new file mode 100644 index 00000000..b067f629 --- /dev/null +++ b/libs/features/overview-route/src/lib/RegisterDeployment/Dialog.tsx @@ -0,0 +1,82 @@ +import { Button, SubmitButton } from '@restate/ui/button'; +import { + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogTrigger, +} from '@restate/ui/dialog'; +import { Icon, IconName } from '@restate/ui/icons'; +import { PropsWithChildren } from 'react'; +import { ErrorBanner } from '@restate/ui/error'; +import { RegistrationForm } from './Form'; + +function RegisterDeploymentFooter({ + isDryRun, + setIsDryRun, + error, + isPending, + formId, +}: { + isDryRun: boolean; + formId: string; + isPending: boolean; + setIsDryRun: (value: boolean) => void; + error?: { + message: string; + restate_code?: string | null; + } | null; +}) { + return ( + +
+ {error && } +
+ {isDryRun ? ( + + + + ) : ( + + )} + + {isDryRun ? 'Next' : 'Confirm'} + +
+
+
+ ); +} + +export function TriggerRegisterDeploymentDialog({ + children = 'Register deployment', +}: PropsWithChildren>) { + return ( + + + + + + {RegisterDeploymentFooter} + + + ); +} diff --git a/libs/features/overview-route/src/lib/RegisterDeployment/Form.tsx b/libs/features/overview-route/src/lib/RegisterDeployment/Form.tsx new file mode 100644 index 00000000..51ba79b9 --- /dev/null +++ b/libs/features/overview-route/src/lib/RegisterDeployment/Form.tsx @@ -0,0 +1,258 @@ +import { Form } from '@remix-run/react'; +import { + useListDeployments, + useRegisterDeployment, +} from '@restate/data-access/admin-api'; +import { useDialog } from '@restate/ui/dialog'; +import { FormFieldCheckbox, FormFieldInput } from '@restate/ui/form-field'; +import { Icon, IconName } from '@restate/ui/icons'; +import { + FormEvent, + PropsWithChildren, + ReactNode, + useEffect, + useId, + useState, +} from 'react'; +import { Radio } from 'react-aria-components'; +import { RadioGroup } from '@restate/ui/radio-group'; +import { RegisterDeploymentResults } from './Results'; +import { AdditionalHeaders } from '../RegisterDeployment/AdditionalHeaders'; +import { DeploymentType } from '../types'; +import { UseHTTP11 } from '../RegisterDeployment/UseHTTP11'; +import { AssumeARNRole } from '../RegisterDeployment/AssumeARNRole'; + +function CustomRadio({ + value, + children, + className, +}: PropsWithChildren<{ + value: string; + className?: string; +}>) { + return ( + `${className} + group relative flex cursor-default rounded-lg shadow-none outline-none bg-clip-padding border + ${ + isFocusVisible + ? 'ring-2 ring-blue-600 ring-offset-1 ring-offset-white/80' + : '' + } + ${ + isSelected + ? `${ + isPressed ? 'bg-gray-50' : 'bg-white' + } border shadow-sm text-gray-800 scale-105 z-10` + : 'border-transparent text-gray-500' + } + ${isPressed && !isSelected ? 'bg-gray-100' : ''} + ${!isSelected && !isPressed ? 'bg-white/50' : ''} + `} + > + {children} + + ); +} +// TODO: change type on paste +// fix autofocus +function RegistrationFormFields({ + children, + className = '', +}: PropsWithChildren<{ className?: string }>) { + const [type, setType] = useState('uri'); + const isURI = type === 'uri'; + const isLambda = type === 'arn'; + + return ( + <> +
+

+ Register deployment +

+

+ Point Restate to your deployed services so Restate can discover and + register your services and handlers +

+
+
+ + Please specify the HTTP endpoint or Lambda identifier: + + } + /> + + Please specify the HTTP endpoint or Lambda identifier: + + } + /> +
+ setType(value as 'uri' | 'arn')} + > + + + + + + + +
+
+ + + Override existing deployments + +
+ + If selected, it will override any existing deployment with the + same URI/identifier, potentially causing unrecoverable errors in + active invocations. + +
+ {isURI && } + {isLambda && } + +
+
+ {children} + + ); +} + +export function RegistrationForm({ + children, +}: { + children: (props: { + isDryRun: boolean; + isPending: boolean; + formId: string; + setIsDryRun: (value: boolean) => void; + error?: { + message: string; + restate_code?: string | null; + } | null; + }) => ReactNode; +}) { + const formId = useId(); + const { close } = useDialog(); + const { refetch } = useListDeployments(); + const [isDryRun, setIsDryRun] = useState(true); + const { mutate, isPending, error, data, reset } = useRegisterDeployment({ + onSuccess: (data, variables) => { + setIsDryRun(false); + if (variables.body?.dry_run === false) { + refetch(); + close(); + } + }, + }); + + useEffect(() => { + return () => { + reset(); + }; + }, [reset]); + + function handleSubmit(event: FormEvent) { + event.preventDefault(); + const formData = new FormData(event.currentTarget); + const uri = String(formData.get('uri')); + const arn = String(formData.get('arn')); + const type = String(formData.get('type')); + const force = formData.get('force') === 'true'; + const use_http_11 = formData.get('use_http_11') === 'true'; + const assume_role_arn = + formData.get('assume_role_arn')?.toString() || undefined; + const keys = formData.getAll('key'); + const values = formData.getAll('value'); + const additional_headers: Record = keys.reduce( + (result, key, index) => { + const value = values.at(index); + if (typeof key === 'string' && typeof value === 'string' && key) { + return { ...result, [key]: value }; + } + return result; + }, + {} + ); + + mutate({ + body: { + ...(type === 'uri' ? { uri, use_http_11 } : { arn, assume_role_arn }), + force, + dry_run: isDryRun, + additional_headers, + }, + }); + } + + return ( +
+ + {!isDryRun && data?.services && ( +
+

+ Deployment {data.id} +

+

+ Below, you will find the list of services and handlers included in + this deployment. Please confirm. +

+ +
+ )} +
+ {children({ isDryRun, isPending, setIsDryRun, error, formId })} +
+ ); +} diff --git a/libs/features/overview-route/src/lib/RegisterDeployment/Results.tsx b/libs/features/overview-route/src/lib/RegisterDeployment/Results.tsx new file mode 100644 index 00000000..2ffe28a9 --- /dev/null +++ b/libs/features/overview-route/src/lib/RegisterDeployment/Results.tsx @@ -0,0 +1,79 @@ +import * as adminApi from '@restate/data-access/admin-api/spec'; +import { Icon, IconName } from '@restate/ui/icons'; + +export function RegisterDeploymentResults({ + services, +}: { + services: adminApi.components['schemas']['ServiceMetadata'][]; +}) { + if (services.length === 0) { + return ( +
+

No services

+

+ This deployment does not expose any services. +

+
+ ); + } + return ( +
+ {services.map((service) => ( + + ))} +
+ ); +} + +function Service({ + service, +}: { + service: adminApi.components['schemas']['ServiceMetadata']; +}) { + return ( +
+
+
+
+ +
+
+
{service.name}
+
+ rev. {service.revision} +
+
+ {service.ty} +
+
+
+
+ Handlers +
+ {service.handlers.map((handler) => ( + + ))} +
+
+ ); +} + +function ServiceHandler({ + handler, +}: { + handler: adminApi.components['schemas']['ServiceMetadata']['handlers'][number]; +}) { + return ( +
+
+
+ +
+
+
{handler.name}
+
+ {handler.ty} +
+
+ ); +} diff --git a/libs/features/overview-route/src/lib/RegisterDeployment/UseHTTP11.tsx b/libs/features/overview-route/src/lib/RegisterDeployment/UseHTTP11.tsx new file mode 100644 index 00000000..712d697e --- /dev/null +++ b/libs/features/overview-route/src/lib/RegisterDeployment/UseHTTP11.tsx @@ -0,0 +1,26 @@ +import { FormFieldCheckbox } from '@restate/ui/form-field'; + +export function UseHTTP11() { + return ( + + + Use HTTP1.1 + +
+ + If selected, discovery will use a client defaulting to{' '} + HTTP1.1. HTTP2 may be used for{' '} + TLS servers advertising HTTP2 support via + ALPN. HTTP1.1 will work only in request-response mode. + +
+ ); +} diff --git a/libs/features/overview-route/src/lib/RestateServer.tsx b/libs/features/overview-route/src/lib/RestateServer.tsx new file mode 100644 index 00000000..557cd732 --- /dev/null +++ b/libs/features/overview-route/src/lib/RestateServer.tsx @@ -0,0 +1,30 @@ +import { Button } from '@restate/ui/button'; +import { PropsWithChildren } from 'react'; + +export function RestateServer({ + className, + children, +}: PropsWithChildren<{ className?: string }>) { + return ( +
+ + {children} +
+ ); +} diff --git a/libs/features/overview-route/src/lib/Service.tsx b/libs/features/overview-route/src/lib/Service.tsx new file mode 100644 index 00000000..9f9a3a39 --- /dev/null +++ b/libs/features/overview-route/src/lib/Service.tsx @@ -0,0 +1,28 @@ +import type { Deployment } from '@restate/data-access/admin-api'; +import { Icon, IconName } from '@restate/ui/icons'; +import { Link } from '@restate/ui/link'; + +export function Service({ + service, +}: { + service: Deployment['services'][number]; +}) { + return ( + +
+
+
+ +
+
+
{service.name}
+
+ rev. {service.revision} +
+
+ + ); +} diff --git a/libs/features/overview-route/src/lib/overview.route.tsx b/libs/features/overview-route/src/lib/overview.route.tsx new file mode 100644 index 00000000..da35fe99 --- /dev/null +++ b/libs/features/overview-route/src/lib/overview.route.tsx @@ -0,0 +1,122 @@ +import { useListDeployments } from '@restate/data-access/admin-api'; +import { RestateServer } from './RestateServer'; +import { Deployment } from './Deployment'; +import { tv } from 'tailwind-variants'; +import { TriggerRegisterDeploymentDialog } from './RegisterDeployment/Dialog'; +import { ServiceDetails } from './Details.tsx/Service'; +import { DeploymentDetails } from './Details.tsx/Deployment'; + +const deploymentsStyles = tv({ + base: 'w-full md:row-start-1 md:col-start-1 grid gap-8 gap-x-20 gap2-x-[calc(8rem+150px)]', + variants: { + isEmpty: { + true: 'gap-0 h-full [grid-template-columns:1fr]', + false: + 'h-fit [grid-template-columns:1fr] md:[grid-template-columns:1fr_150px_1fr]', + }, + }, + defaultVariants: { + isEmpty: false, + }, +}); +const reactServerStyles = tv({ + base: 'justify-center flex md:sticky md:top-[11rem] flex-col items-center w-fit', + variants: { + isEmpty: { + true: 'md:h-[calc(100vh-150px-6rem)] py-8 flex-auto w-full justify-center rounded-xl border bg-gray-200/50 shadow-[inset_0_1px_0px_0px_rgba(0,0,0,0.03)]', + false: + 'h-fit md:max-h-[calc(100vh-150px-6rem)] min-h-[min(100%,calc(100vh-150px-6rem))]', + }, + }, + defaultVariants: { + isEmpty: false, + }, +}); + +function MultipleDeploymentsPlaceholder() { + return ( +
+
+ + Deployment + +
+
+ ); +} + +function OneDeploymentPlaceholder() { + return ( +
+

+ Point Restate to your deployed services so Restate can discover and + register your services and handlers +

+
+ +
+
+ ); +} + +function NoDeploymentPlaceholder() { + return ( +
+

No deployments

+

+ Point Restate to your deployed services so Restate can discover and + register your services and handlers +

+
+ +
+
+ ); +} + +// TODO: refactor layout +function Component() { + const { data, isError, isLoading, isSuccess } = useListDeployments(); + + // Handle isLoading & isError + const deployments = data?.deployments.slice(0, 60) ?? []; + const hasNoDeployment = isSuccess && deployments.length === 0; + + return ( + <> +
+
+
+ {deployments.map((deployment, i) => ( + + ))} +
+ + {hasNoDeployment && } + {deployments.length > 1 && } + +
+ {deployments.map((deployment, i) => ( + + ))} + {deployments.length === 1 && } +
+
+
+ + + + ); +} + +export const overview = { Component }; diff --git a/libs/features/overview-route/src/lib/restate-server.svg b/libs/features/overview-route/src/lib/restate-server.svg new file mode 100644 index 00000000..07318fb6 --- /dev/null +++ b/libs/features/overview-route/src/lib/restate-server.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/libs/features/overview-route/src/lib/types.ts b/libs/features/overview-route/src/lib/types.ts new file mode 100644 index 00000000..dfb0293b --- /dev/null +++ b/libs/features/overview-route/src/lib/types.ts @@ -0,0 +1,15 @@ +import type { Deployment } from '@restate/data-access/admin-api'; + +export type HTTPDeployment = Exclude; +export type LambdaDeployment = Exclude; +export type DeploymentType = 'uri' | 'arn'; +export function isHttpDeployment( + deployment: Deployment +): deployment is HTTPDeployment { + return 'uri' in deployment; +} +export function isLambdaDeployment( + deployment: Deployment +): deployment is LambdaDeployment { + return 'arn' in deployment; +} diff --git a/libs/features/overview-route/tsconfig.json b/libs/features/overview-route/tsconfig.json new file mode 100644 index 00000000..4daaf45c --- /dev/null +++ b/libs/features/overview-route/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json" +} diff --git a/libs/features/overview-route/tsconfig.lib.json b/libs/features/overview-route/tsconfig.lib.json new file mode 100644 index 00000000..8baf71b1 --- /dev/null +++ b/libs/features/overview-route/tsconfig.lib.json @@ -0,0 +1,23 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": [ + "node", + + "@nx/react/typings/cssmodule.d.ts", + "@nx/react/typings/image.d.ts" + ] + }, + "exclude": [ + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx" + ], + "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] +} diff --git a/libs/features/overview-route/tsconfig.spec.json b/libs/features/overview-route/tsconfig.spec.json new file mode 100644 index 00000000..05a0e183 --- /dev/null +++ b/libs/features/overview-route/tsconfig.spec.json @@ -0,0 +1,26 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ] + }, + "include": [ + "vite.config.ts", + "vitest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] +} diff --git a/libs/features/overview-route/vite.config.ts b/libs/features/overview-route/vite.config.ts new file mode 100644 index 00000000..e281200f --- /dev/null +++ b/libs/features/overview-route/vite.config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + +export default defineConfig({ + root: __dirname, + cacheDir: '../../../node_modules/.vite/libs/features/overview-route', + + plugins: [react(), nxViteTsPaths()], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + test: { + watch: false, + globals: true, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../../coverage/libs/features/overview-route', + provider: 'v8', + }, + }, +}); diff --git a/libs/ui/button/.babelrc b/libs/ui/button/.babelrc new file mode 100644 index 00000000..1ea870ea --- /dev/null +++ b/libs/ui/button/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/ui/button/.eslintrc.json b/libs/ui/button/.eslintrc.json new file mode 100644 index 00000000..75b85077 --- /dev/null +++ b/libs/ui/button/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nx/react", "../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/ui/button/README.md b/libs/ui/button/README.md new file mode 100644 index 00000000..ce86b388 --- /dev/null +++ b/libs/ui/button/README.md @@ -0,0 +1,7 @@ +# ui-button + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test ui-button` to execute the unit tests via [Vitest](https://vitest.dev/). diff --git a/libs/ui/button/project.json b/libs/ui/button/project.json new file mode 100644 index 00000000..75237409 --- /dev/null +++ b/libs/ui/button/project.json @@ -0,0 +1,9 @@ +{ + "name": "button", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/ui/button/src", + "projectType": "library", + "tags": [], + "// targets": "to see all targets run: nx show project ui-button --web", + "targets": {} +} diff --git a/libs/ui/button/src/index.ts b/libs/ui/button/src/index.ts new file mode 100644 index 00000000..5adaee4d --- /dev/null +++ b/libs/ui/button/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/Button'; +export * from './lib/SubmitButton'; diff --git a/libs/ui/button/src/lib/Button.spec.tsx b/libs/ui/button/src/lib/Button.spec.tsx new file mode 100644 index 00000000..db234ce5 --- /dev/null +++ b/libs/ui/button/src/lib/Button.spec.tsx @@ -0,0 +1,9 @@ +import { render } from '@testing-library/react'; +import { Button } from './Button'; + +describe('Button', () => { + it('should render successfully', () => { + const { baseElement } = render( + ); +} diff --git a/libs/ui/button/src/test-setup.ts b/libs/ui/button/src/test-setup.ts new file mode 100644 index 00000000..85205829 --- /dev/null +++ b/libs/ui/button/src/test-setup.ts @@ -0,0 +1,3 @@ +import { installGlobals } from '@remix-run/node'; +import '@testing-library/jest-dom/matchers'; +installGlobals(); diff --git a/libs/ui/button/tsconfig.json b/libs/ui/button/tsconfig.json new file mode 100644 index 00000000..afbd8a58 --- /dev/null +++ b/libs/ui/button/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json" +} diff --git a/libs/ui/button/tsconfig.lib.json b/libs/ui/button/tsconfig.lib.json new file mode 100644 index 00000000..1454bbe7 --- /dev/null +++ b/libs/ui/button/tsconfig.lib.json @@ -0,0 +1,22 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": [ + "node", + "@nx/react/typings/cssmodule.d.ts", + "@nx/react/typings/image.d.ts" + ] + }, + "exclude": [ + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx" + ], + "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] +} diff --git a/libs/ui/button/tsconfig.spec.json b/libs/ui/button/tsconfig.spec.json new file mode 100644 index 00000000..05a0e183 --- /dev/null +++ b/libs/ui/button/tsconfig.spec.json @@ -0,0 +1,26 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ] + }, + "include": [ + "vite.config.ts", + "vitest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] +} diff --git a/libs/ui/button/vite.config.ts b/libs/ui/button/vite.config.ts new file mode 100644 index 00000000..074ea223 --- /dev/null +++ b/libs/ui/button/vite.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + +export default defineConfig({ + root: __dirname, + cacheDir: '../../../node_modules/.vite/libs/ui/button', + + plugins: [react(), nxViteTsPaths()], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + test: { + setupFiles: ['./src/test-setup.ts'], + globals: true, + cache: { dir: '../../../node_modules/.vitest' }, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { reportsDirectory: '../../../coverage/libs/ui/button', provider: 'v8' }, + }, +}); diff --git a/libs/ui/code/.babelrc b/libs/ui/code/.babelrc new file mode 100644 index 00000000..1ea870ea --- /dev/null +++ b/libs/ui/code/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/ui/code/.eslintrc.json b/libs/ui/code/.eslintrc.json new file mode 100644 index 00000000..75b85077 --- /dev/null +++ b/libs/ui/code/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nx/react", "../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/ui/code/README.md b/libs/ui/code/README.md new file mode 100644 index 00000000..9422076d --- /dev/null +++ b/libs/ui/code/README.md @@ -0,0 +1,7 @@ +# code + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test code` to execute the unit tests via [Vitest](https://vitest.dev/). diff --git a/libs/ui/code/project.json b/libs/ui/code/project.json new file mode 100644 index 00000000..2f72871b --- /dev/null +++ b/libs/ui/code/project.json @@ -0,0 +1,9 @@ +{ + "name": "code", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/ui/code/src", + "projectType": "library", + "tags": [], + "// targets": "to see all targets run: nx show project code --web", + "targets": {} +} diff --git a/libs/ui/code/src/index.ts b/libs/ui/code/src/index.ts new file mode 100644 index 00000000..dc6e3b60 --- /dev/null +++ b/libs/ui/code/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/Code'; +export * from './lib/Snippet'; diff --git a/libs/ui/code/src/lib/Code.tsx b/libs/ui/code/src/lib/Code.tsx new file mode 100644 index 00000000..41c488c8 --- /dev/null +++ b/libs/ui/code/src/lib/Code.tsx @@ -0,0 +1,14 @@ +import { PropsWithChildren } from 'react'; +import { tv } from 'tailwind-variants'; + +interface CodeProps { + className?: string; +} + +const styles = tv({ + base: 'group/code flex flex-col gap-2 gap-y-0 items-stretch font-mono [overflow-wrap:anywhere] rounded-xl border bg-gray-200/50 shadow-[inset_0_1px_0px_0px_rgba(0,0,0,0.03)] text-code p-2 sm:py-3 whitespace-break-spaces', +}); + +export function Code({ children, className }: PropsWithChildren) { + return {children}; +} diff --git a/libs/ui/code/src/lib/Snippet.tsx b/libs/ui/code/src/lib/Snippet.tsx new file mode 100644 index 00000000..96a49c9f --- /dev/null +++ b/libs/ui/code/src/lib/Snippet.tsx @@ -0,0 +1,145 @@ +import { Button } from '@restate/ui/button'; +import { Icon, IconName } from '@restate/ui/icons'; +import { Children, PropsWithChildren, ReactNode, memo, useState } from 'react'; +import { tv } from 'tailwind-variants'; +import { syntaxHighlighter } from './SyntaxHighlighter'; +import { Nav, NavButtonItem } from '@restate/ui/nav'; + +interface SnippetProps { + className?: string; + language?: 'typescript' | 'java' | 'json' | 'bash'; +} + +const LANGUAGE_LABEL: Record< + Exclude, + string +> = { + typescript: 'Typescript', + java: 'Java', + json: 'json', + bash: 'bash', +}; + +function SyntaxHighlighter({ + code, + language, +}: { + code: string; + language: Exclude; +}) { + return ( + + ); +} + +const OptimizedSyntaxHighlighter = memo(SyntaxHighlighter); + +const snippetStyles = tv({ + base: 'flex gap-2 gap-x-2 items-start group/snippet p-2 py-0 has-[.copy]:-my-1 has-[.copy]:pr-1 [&:not(:has(.copy))]:group-has-[.copy]/code:pr-16 [&_.copy]:-mr-2', +}); +export function Snippet({ + children, + className, + language = 'bash', +}: PropsWithChildren) { + const childrenArray = Children.toArray(children); + const codes = childrenArray + .filter((child) => typeof child === 'string') + .join(''); + const others = childrenArray.filter((child) => typeof child !== 'string'); + return ( + + + {others} + + ); +} + +interface SnippetCopyProps { + className?: string; + copyText: string; +} + +const snippetCopyStyles = tv({ + base: 'copy flex-shrink-0 flex items-center gap-1 ml-auto p-2 text-xs', +}); +export function SnippetCopy({ + className, + copyText, +}: PropsWithChildren) { + const [isCopied, setIsCopied] = useState(false); + + return ( + + ); +} + +const snippetTabsStyles = tv({ + base: 'relative @container', +}); +export function SnippetTabs({ + children, + className, + languages, + defaultLanguage, +}: { + className?: string; + languages: Exclude[]; + defaultLanguage: Exclude; + children: ( + language: Exclude + ) => ReactNode; +}) { + const [currentLanguage, setCurrentLanguage] = + useState<(typeof languages)[number]>(defaultLanguage); + return ( +
+
+ +
+
{children(currentLanguage)}
+
+ ); +} diff --git a/libs/ui/code/src/lib/SyntaxHighlighter.tsx b/libs/ui/code/src/lib/SyntaxHighlighter.tsx new file mode 100644 index 00000000..8b590b35 --- /dev/null +++ b/libs/ui/code/src/lib/SyntaxHighlighter.tsx @@ -0,0 +1,13 @@ +import hljs from 'highlight.js/lib/core'; +import typescript from 'highlight.js/lib/languages/typescript'; +import java from 'highlight.js/lib/languages/java'; +import bash from 'highlight.js/lib/languages/bash'; +import json from 'highlight.js/lib/languages/json'; +import 'highlight.js/styles/mono-blue.css'; + +hljs.registerLanguage('typescript', typescript); +hljs.registerLanguage('bash', bash); +hljs.registerLanguage('java', java); +hljs.registerLanguage('json', json); + +export const syntaxHighlighter = hljs; diff --git a/libs/ui/code/tsconfig.json b/libs/ui/code/tsconfig.json new file mode 100644 index 00000000..4daaf45c --- /dev/null +++ b/libs/ui/code/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json" +} diff --git a/libs/ui/code/tsconfig.lib.json b/libs/ui/code/tsconfig.lib.json new file mode 100644 index 00000000..8baf71b1 --- /dev/null +++ b/libs/ui/code/tsconfig.lib.json @@ -0,0 +1,23 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": [ + "node", + + "@nx/react/typings/cssmodule.d.ts", + "@nx/react/typings/image.d.ts" + ] + }, + "exclude": [ + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx" + ], + "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] +} diff --git a/libs/ui/code/tsconfig.spec.json b/libs/ui/code/tsconfig.spec.json new file mode 100644 index 00000000..05a0e183 --- /dev/null +++ b/libs/ui/code/tsconfig.spec.json @@ -0,0 +1,26 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ] + }, + "include": [ + "vite.config.ts", + "vitest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] +} diff --git a/libs/ui/code/vite.config.ts b/libs/ui/code/vite.config.ts new file mode 100644 index 00000000..149b8953 --- /dev/null +++ b/libs/ui/code/vite.config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + +export default defineConfig({ + root: __dirname, + cacheDir: '../../../node_modules/.vite/libs/ui/code', + + plugins: [react(), nxViteTsPaths()], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + test: { + globals: true, + cache: { dir: '../../../node_modules/.vitest' }, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../../coverage/libs/ui/code', + provider: 'v8', + }, + }, +}); diff --git a/libs/ui/details/.babelrc b/libs/ui/details/.babelrc new file mode 100644 index 00000000..1ea870ea --- /dev/null +++ b/libs/ui/details/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/ui/details/.eslintrc.json b/libs/ui/details/.eslintrc.json new file mode 100644 index 00000000..75b85077 --- /dev/null +++ b/libs/ui/details/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nx/react", "../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/ui/details/README.md b/libs/ui/details/README.md new file mode 100644 index 00000000..5d50192c --- /dev/null +++ b/libs/ui/details/README.md @@ -0,0 +1,7 @@ +# details + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test details` to execute the unit tests via [Vitest](https://vitest.dev/). diff --git a/libs/ui/details/project.json b/libs/ui/details/project.json new file mode 100644 index 00000000..b8db241c --- /dev/null +++ b/libs/ui/details/project.json @@ -0,0 +1,9 @@ +{ + "name": "details", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/ui/details/src", + "projectType": "library", + "tags": [], + "// targets": "to see all targets run: nx show project details --web", + "targets": {} +} diff --git a/libs/ui/details/src/index.ts b/libs/ui/details/src/index.ts new file mode 100644 index 00000000..5f1dc91d --- /dev/null +++ b/libs/ui/details/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/Details'; +export * from './lib/Summary'; diff --git a/libs/ui/details/src/lib/Details.tsx b/libs/ui/details/src/lib/Details.tsx new file mode 100644 index 00000000..7365d960 --- /dev/null +++ b/libs/ui/details/src/lib/Details.tsx @@ -0,0 +1,49 @@ +import { Children, PropsWithChildren, useId } from 'react'; +import { tv } from 'tailwind-variants'; +import { DetailsProvider } from './DetailsContext'; +import { isSummary } from './Summary'; + +interface DetailsProps { + open?: boolean; + className?: string; + disabled?: boolean; +} + +const styles = tv({ + base: 'group bg-white rounded-xl border text-gray-800 shadow-sm p-1 has-[+details]:rounded-b-none has-[+details]:border-b-0 [&+details]:rounded-t-none [&:not([open]):has(+details)>summary]:rounded-b-none [&[open]>summary]:rounded-b-none [&+details>summary]:rounded-t-none', + variants: { + isDisabled: { + false: '', + true: '[&>summary]:pointer-events-none cursor-not-allowed', + }, + }, + defaultVariants: { + isDisabled: false, + }, +}); + +export function Details({ + children, + open, + className, + disabled, +}: PropsWithChildren) { + const id = useId(); + const childrenArray = Children.toArray(children); + const summary = childrenArray.filter(isSummary); + const detailChildren = childrenArray.filter((child) => !isSummary(child)); + + return ( + +
+ {summary} +
+ {detailChildren} +
+
+
+ ); +} diff --git a/libs/ui/details/src/lib/DetailsContext.tsx b/libs/ui/details/src/lib/DetailsContext.tsx new file mode 100644 index 00000000..a4c3621a --- /dev/null +++ b/libs/ui/details/src/lib/DetailsContext.tsx @@ -0,0 +1,18 @@ +import { PropsWithChildren, createContext, useContext } from 'react'; + +const DetailsContext = createContext({ id: '' }); + +export function DetailsProvider({ + id, + children, +}: PropsWithChildren<{ id: string }>) { + return ( + {children} + ); +} + +export function useSummaryElement() { + const { id } = useContext(DetailsContext); + const element = document.getElementById(id); + return element; +} diff --git a/libs/ui/details/src/lib/Summary.tsx b/libs/ui/details/src/lib/Summary.tsx new file mode 100644 index 00000000..38befefa --- /dev/null +++ b/libs/ui/details/src/lib/Summary.tsx @@ -0,0 +1,51 @@ +import { Children, PropsWithChildren } from 'react'; +import { usePress } from '@react-aria/interactions'; +import { useFocusRing } from 'react-aria'; +import { focusRing } from '@restate/ui/focus'; +import { tv } from 'tailwind-variants'; +import { Icon, IconName } from '@restate/ui/icons'; + +interface SummaryProps { + className?: string; +} + +const summaryStyles = tv({ + extend: focusRing, + base: 'flex gap-2 px-3 py-2 pressed:bg-gray-200 hover:bg-gray-100 rounded-[calc(.75rem_-_1px_-.25rem)] list-none group-open:mb-2 pr-2.5 [&::-webkit-details-marker]:hidden cursor-default', +}); + +export function Summary({ + children, + className, +}: PropsWithChildren) { + // const element = useSummaryElement(); + const { pressProps, isPressed } = usePress({ + onPress: (event) => { + if (event.pointerType === 'keyboard') { + const details = event.target.closest('details'); + if (details instanceof HTMLDetailsElement) { + details.open = !details.open; + } + } + }, + }); + const { isFocusVisible, focusProps } = useFocusRing(); + return ( + +
{children}
+ +
+ ); +} + +export function isSummary(child: ReturnType[number]) { + return typeof child === 'object' && 'type' in child && child.type === Summary; +} diff --git a/libs/ui/details/tsconfig.json b/libs/ui/details/tsconfig.json new file mode 100644 index 00000000..4daaf45c --- /dev/null +++ b/libs/ui/details/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json" +} diff --git a/libs/ui/details/tsconfig.lib.json b/libs/ui/details/tsconfig.lib.json new file mode 100644 index 00000000..8baf71b1 --- /dev/null +++ b/libs/ui/details/tsconfig.lib.json @@ -0,0 +1,23 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": [ + "node", + + "@nx/react/typings/cssmodule.d.ts", + "@nx/react/typings/image.d.ts" + ] + }, + "exclude": [ + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx" + ], + "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] +} diff --git a/libs/ui/details/tsconfig.spec.json b/libs/ui/details/tsconfig.spec.json new file mode 100644 index 00000000..05a0e183 --- /dev/null +++ b/libs/ui/details/tsconfig.spec.json @@ -0,0 +1,26 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ] + }, + "include": [ + "vite.config.ts", + "vitest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] +} diff --git a/libs/ui/details/vite.config.ts b/libs/ui/details/vite.config.ts new file mode 100644 index 00000000..25024931 --- /dev/null +++ b/libs/ui/details/vite.config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + +export default defineConfig({ + root: __dirname, + cacheDir: '../../../node_modules/.vite/libs/ui/details', + + plugins: [react(), nxViteTsPaths()], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + test: { + globals: true, + cache: { dir: '../../../node_modules/.vitest' }, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../../coverage/libs/ui/details', + provider: 'v8', + }, + }, +}); diff --git a/libs/ui/dialog/.babelrc b/libs/ui/dialog/.babelrc new file mode 100644 index 00000000..1ea870ea --- /dev/null +++ b/libs/ui/dialog/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/ui/dialog/.eslintrc.json b/libs/ui/dialog/.eslintrc.json new file mode 100644 index 00000000..75b85077 --- /dev/null +++ b/libs/ui/dialog/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nx/react", "../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/ui/dialog/README.md b/libs/ui/dialog/README.md new file mode 100644 index 00000000..8f2c7425 --- /dev/null +++ b/libs/ui/dialog/README.md @@ -0,0 +1,7 @@ +# ui-dialog + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test ui-dialog` to execute the unit tests via [Vitest](https://vitest.dev/). diff --git a/libs/ui/dialog/project.json b/libs/ui/dialog/project.json new file mode 100644 index 00000000..d2ebdf9d --- /dev/null +++ b/libs/ui/dialog/project.json @@ -0,0 +1,9 @@ +{ + "name": "dialog", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/ui/dialog/src", + "projectType": "library", + "tags": [], + "// targets": "to see all targets run: nx show project ui-dialog --web", + "targets": {} +} diff --git a/libs/ui/dialog/src/index.ts b/libs/ui/dialog/src/index.ts new file mode 100644 index 00000000..badcc051 --- /dev/null +++ b/libs/ui/dialog/src/index.ts @@ -0,0 +1,6 @@ +export * from './lib/Dialog'; +export * from './lib/DialogContent'; +export * from './lib/DialogTrigger'; +export * from './lib/DialogClose'; +export * from './lib/useDialog'; +export { DialogFooter } from './lib/DialogFooter'; diff --git a/libs/ui/dialog/src/lib/Dialog.tsx b/libs/ui/dialog/src/lib/Dialog.tsx new file mode 100644 index 00000000..f98ef782 --- /dev/null +++ b/libs/ui/dialog/src/lib/Dialog.tsx @@ -0,0 +1,19 @@ +import { PropsWithChildren } from 'react'; +import { DialogTrigger } from 'react-aria-components'; + +interface DialogProps { + open?: boolean; + onOpenChange?: (isOpen: boolean) => void; +} + +export function Dialog({ + children, + open, + onOpenChange, +}: PropsWithChildren) { + return ( + + {children} + + ); +} diff --git a/libs/ui/dialog/src/lib/DialogClose.tsx b/libs/ui/dialog/src/lib/DialogClose.tsx new file mode 100644 index 00000000..2a1a992b --- /dev/null +++ b/libs/ui/dialog/src/lib/DialogClose.tsx @@ -0,0 +1,21 @@ +import { ComponentProps, useContext } from 'react'; +import { OverlayTriggerStateContext } from 'react-aria-components'; +import { Pressable, PressResponder } from '@react-aria/interactions'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface DialogCLoseProps { + children: ComponentProps['children']; +} + +export function DialogClose({ children }: DialogCLoseProps) { + const state = useContext(OverlayTriggerStateContext); + return ( + { + state.close(); + }} + > + {children} + + ); +} diff --git a/libs/ui/dialog/src/lib/DialogContent.tsx b/libs/ui/dialog/src/lib/DialogContent.tsx new file mode 100644 index 00000000..9f8a336f --- /dev/null +++ b/libs/ui/dialog/src/lib/DialogContent.tsx @@ -0,0 +1,61 @@ +import type { PropsWithChildren } from 'react'; +import { + Dialog as AriaDialog, + Modal as AriaModal, + ModalOverlay as AriaModalOverlay, + composeRenderProps, +} from 'react-aria-components'; +import { tv } from 'tailwind-variants'; +import { DialogFooterContainer } from './DialogFooter'; + +const overlayStyles = tv({ + base: 'fixed top-0 left-0 w-full isolate z-50 bg-gray-800 bg-opacity-30 transition-opacity flex items-center justify-center p-4 text-center [height:100vh] [min-height:100vh]', + variants: { + isEntering: { + true: 'animate-in fade-in duration-200 ease-out', + }, + isExiting: { + true: 'animate-out fade-out duration-200 ease-in', + }, + }, +}); + +const modalStyles = tv({ + base: 'flex w-full max-w-sm max-h-full overflow-auto rounded-[1.125rem] [clip-path:inset(0_0_0_0_round_1.125rem)] bg-white text-left align-middle text-slate-700 shadow-lg shadow-zinc-800/5 border border-black/5', + variants: { + isEntering: { + true: 'animate-in zoom-in-105 ease-out duration-200', + }, + isExiting: { + true: 'animate-out zoom-out-95 ease-in duration-200', + }, + }, +}); + +interface DialogContentProps { + className?: string; +} + +export function DialogContent({ + children, + className, +}: PropsWithChildren) { + return ( + + + modalStyles({ ...renderProps, className }) + )} + > + + +
+ {children} +
+
+
+
+
+ ); +} diff --git a/libs/ui/dialog/src/lib/DialogFooter.tsx b/libs/ui/dialog/src/lib/DialogFooter.tsx new file mode 100644 index 00000000..4edb5b20 --- /dev/null +++ b/libs/ui/dialog/src/lib/DialogFooter.tsx @@ -0,0 +1,41 @@ +import { + PropsWithChildren, + createContext, + useContext, + useId, + useState, +} from 'react'; +import { createPortal } from 'react-dom'; + +const DialogFooterContext = createContext<{ container: HTMLElement | null }>({ + container: null, +}); + +export function DialogFooterContainer({ children }: PropsWithChildren) { + const id = useId(); + const [container, setContainer] = useState(null); + + return ( + + {children} +
+ + ); +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface DialogFooterProps {} +export function DialogFooter({ + children, +}: PropsWithChildren) { + const { container } = useContext(DialogFooterContext); + + if (container) { + return createPortal(children, container); + } + return null; +} diff --git a/libs/ui/dialog/src/lib/DialogTrigger.tsx b/libs/ui/dialog/src/lib/DialogTrigger.tsx new file mode 100644 index 00000000..e79c5e51 --- /dev/null +++ b/libs/ui/dialog/src/lib/DialogTrigger.tsx @@ -0,0 +1,9 @@ +import { PropsWithChildren } from 'react'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface DialogTriggerProps {} +export function DialogTrigger({ + children, +}: PropsWithChildren) { + return children; +} diff --git a/libs/ui/dialog/src/lib/useDialog.ts b/libs/ui/dialog/src/lib/useDialog.ts new file mode 100644 index 00000000..7076c0e9 --- /dev/null +++ b/libs/ui/dialog/src/lib/useDialog.ts @@ -0,0 +1,8 @@ +import { useContext } from 'react'; +import { OverlayTriggerStateContext } from 'react-aria-components'; + +export function useDialog() { + const { open, close, isOpen } = useContext(OverlayTriggerStateContext); + + return { open, close, isOpen }; +} diff --git a/libs/ui/dialog/tsconfig.json b/libs/ui/dialog/tsconfig.json new file mode 100644 index 00000000..4daaf45c --- /dev/null +++ b/libs/ui/dialog/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json" +} diff --git a/libs/ui/dialog/tsconfig.lib.json b/libs/ui/dialog/tsconfig.lib.json new file mode 100644 index 00000000..8baf71b1 --- /dev/null +++ b/libs/ui/dialog/tsconfig.lib.json @@ -0,0 +1,23 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": [ + "node", + + "@nx/react/typings/cssmodule.d.ts", + "@nx/react/typings/image.d.ts" + ] + }, + "exclude": [ + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx" + ], + "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] +} diff --git a/libs/ui/dialog/tsconfig.spec.json b/libs/ui/dialog/tsconfig.spec.json new file mode 100644 index 00000000..05a0e183 --- /dev/null +++ b/libs/ui/dialog/tsconfig.spec.json @@ -0,0 +1,26 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ] + }, + "include": [ + "vite.config.ts", + "vitest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] +} diff --git a/libs/ui/dialog/vite.config.ts b/libs/ui/dialog/vite.config.ts new file mode 100644 index 00000000..db44a463 --- /dev/null +++ b/libs/ui/dialog/vite.config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + +export default defineConfig({ + root: __dirname, + cacheDir: '../../../node_modules/.vite/libs/ui/modal', + + plugins: [react(), nxViteTsPaths()], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + test: { + globals: true, + cache: { dir: '../../../node_modules/.vitest' }, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../../coverage/libs/ui/modal', + provider: 'v8', + }, + }, +}); diff --git a/libs/ui/dropdown/.babelrc b/libs/ui/dropdown/.babelrc new file mode 100644 index 00000000..1ea870ea --- /dev/null +++ b/libs/ui/dropdown/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/ui/dropdown/.eslintrc.json b/libs/ui/dropdown/.eslintrc.json new file mode 100644 index 00000000..75b85077 --- /dev/null +++ b/libs/ui/dropdown/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nx/react", "../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/ui/dropdown/README.md b/libs/ui/dropdown/README.md new file mode 100644 index 00000000..75a68b9f --- /dev/null +++ b/libs/ui/dropdown/README.md @@ -0,0 +1,7 @@ +# ui-dropdown + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test ui-dropdown` to execute the unit tests via [Vitest](https://vitest.dev/). diff --git a/libs/ui/dropdown/project.json b/libs/ui/dropdown/project.json new file mode 100644 index 00000000..eb692e94 --- /dev/null +++ b/libs/ui/dropdown/project.json @@ -0,0 +1,9 @@ +{ + "name": "dropdown", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/ui/dropdown/src", + "projectType": "library", + "tags": [], + "// targets": "to see all targets run: nx show project ui-dropdown --web", + "targets": {} +} diff --git a/libs/ui/dropdown/src/index.ts b/libs/ui/dropdown/src/index.ts new file mode 100644 index 00000000..f83df1f3 --- /dev/null +++ b/libs/ui/dropdown/src/index.ts @@ -0,0 +1,7 @@ +export { DropdownMenu } from './lib/DropdownMenu'; +export { DropdownItem } from './lib/DropdownItem'; +export { DropdownSection } from './lib/DropdownSection'; +export { DropdownSeparator } from './lib/DropdownSeparator'; +export { DropdownTrigger } from './lib/DropdownTrigger'; +export { DropdownPopover } from './lib/DropdownPopover'; +export { Dropdown } from './lib/Dropdown'; diff --git a/libs/ui/dropdown/src/lib/Dropdown.tsx b/libs/ui/dropdown/src/lib/Dropdown.tsx new file mode 100644 index 00000000..d781f8ec --- /dev/null +++ b/libs/ui/dropdown/src/lib/Dropdown.tsx @@ -0,0 +1,9 @@ +import { PropsWithChildren } from 'react'; +import { MenuTrigger } from 'react-aria-components'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface DropdownProps {} + +export function Dropdown({ children }: PropsWithChildren) { + return {children}; +} diff --git a/libs/ui/dropdown/src/lib/DropdownItem.tsx b/libs/ui/dropdown/src/lib/DropdownItem.tsx new file mode 100644 index 00000000..7c4d4a36 --- /dev/null +++ b/libs/ui/dropdown/src/lib/DropdownItem.tsx @@ -0,0 +1,113 @@ +import { Icon, IconName } from '@restate/ui/icons'; +import type { PropsWithChildren } from 'react'; +import { + MenuItem as AriaMenuItem, + MenuItemProps as AriaMenuItemProps, + composeRenderProps, +} from 'react-aria-components'; +import { tv } from 'tailwind-variants'; + +const styles = tv({ + base: 'group dropdown-item flex rounded-xl items-center gap-4 cursor-default select-none py-2 px-3 outline outline-0 text-sm forced-color-adjust-none', + variants: { + isDisabled: { + false: 'text-gray-900 dark:text-zinc-100', + true: 'text-gray-300 dark:text-zinc-600 forced-colors:text-[GrayText]', + }, + isFocused: { + true: 'bg-blue-600 text-white forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]', + }, + }, +}); + +const destructiveStyles = tv({ + base: 'group dropdown-item flex rounded-xl items-center gap-4 cursor-default select-none py-2 px-3 outline outline-0 text-sm forced-color-adjust-none', + variants: { + isDisabled: { + false: 'text-red-600 dark:text-zinc-100', + true: 'text-gray-300 dark:text-zinc-600 forced-colors:text-[GrayText]', + }, + isFocused: { + true: 'bg-red-600 text-white forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]', + }, + }, +}); + +function StyledDropdownItem({ + destructive, + ...props +}: AriaMenuItemProps & { destructive?: boolean }) { + return ( + + {composeRenderProps( + props.children, + (children, { selectionMode, isSelected }) => ( + <> + + {children} + + {selectionMode !== 'none' && ( + + {isSelected && } + + )} + + ) + )} + + ); +} + +interface DropdownItemProps + extends PropsWithChildren<{ + value?: never; + href?: never; + }> { + destructive?: boolean; + className?: string; +} + +interface DropdownCustomItemProps + extends PropsWithChildren< + Omit + > { + value: string; + href?: never; +} + +interface DropdownNavItemProps + extends Omit { + href: string; + value?: string; +} + +function isNavItem( + props: DropdownItemProps | DropdownCustomItemProps | DropdownNavItemProps +): props is DropdownNavItemProps { + return Boolean(props.href); +} + +function isCustomItem( + props: DropdownItemProps | DropdownCustomItemProps | DropdownNavItemProps +): props is DropdownCustomItemProps { + return typeof props.value === 'string'; +} + +export function DropdownItem( + props: DropdownItemProps | DropdownCustomItemProps | DropdownNavItemProps +) { + if (isNavItem(props)) { + const { href, value, ...rest } = props; + return ( + + ); + } + if (isCustomItem(props)) { + const { value, ...rest } = props; + return ; + } + return ; +} diff --git a/libs/ui/dropdown/src/lib/DropdownMenu.tsx b/libs/ui/dropdown/src/lib/DropdownMenu.tsx new file mode 100644 index 00000000..926086b1 --- /dev/null +++ b/libs/ui/dropdown/src/lib/DropdownMenu.tsx @@ -0,0 +1,68 @@ +import { + Menu as AriaMenu, + MenuProps as AriaMenuProps, +} from 'react-aria-components'; +import { PropsWithChildren } from 'react'; +import { tv } from 'tailwind-variants'; + +const styles = tv({ + base: 'p-1 outline outline-0 max-h-[inherit] overflow-auto [clip-path:inset(0_0_0_0_round_.75rem)] [&~.dropdown-menu]:pt-0', +}); +function StyledDropdownMenu({ + className, + ...props +}: AriaMenuProps) { + return ( + + ); +} + +export interface DropdownMenuProps { + disabledItems?: Iterable; + ['aria-label']?: string; + selectable?: never; + selectedItems?: never; + multiple?: never; + onSelect?: (key: string) => void; + className?: string; + autoFocus?: boolean; +} + +export interface SelectableDropdownMenuProps + extends Omit { + multiple?: boolean; + selectedItems?: Iterable; + selectable: true; +} + +export function DropdownMenu({ + multiple, + disabledItems, + selectedItems, + selectable, + onSelect, + className, + ...props +}: PropsWithChildren) { + if (selectable) { + return ( + onSelect?.(String(key))} + className={className} + /> + ); + } else { + return ( + onSelect?.(String(key))} + className={className} + /> + ); + } +} diff --git a/libs/ui/dropdown/src/lib/DropdownPopover.tsx b/libs/ui/dropdown/src/lib/DropdownPopover.tsx new file mode 100644 index 00000000..2c487836 --- /dev/null +++ b/libs/ui/dropdown/src/lib/DropdownPopover.tsx @@ -0,0 +1,22 @@ +import { PopoverContent } from '@restate/ui/popover'; +import type { PropsWithChildren } from 'react'; +import { tv } from 'tailwind-variants'; + +interface DropdownPopoverProps { + className?: string; +} + +const styles = tv({ + base: 'min-w-[150px]', +}); + +export function DropdownPopover({ + children, + className, +}: PropsWithChildren) { + return ( + + {children} + + ); +} diff --git a/libs/ui/dropdown/src/lib/DropdownSection.tsx b/libs/ui/dropdown/src/lib/DropdownSection.tsx new file mode 100644 index 00000000..356b438f --- /dev/null +++ b/libs/ui/dropdown/src/lib/DropdownSection.tsx @@ -0,0 +1,30 @@ +import { PropsWithChildren } from 'react'; +import { Header } from 'react-aria-components'; +import { tv } from 'tailwind-variants'; + +export interface DropdownSectionProps extends PropsWithChildren { + title?: string; + className?: string; +} + +const styles = tv({ + slots: { + container: 'px-1', + header: 'text-sm font-semibold text-gray-500 px-4 py-1 pt-2 truncate', + menu: 'bg-white rounded-xl border [&_.dropdown-item]:rounded-lg', + }, +}); +export function DropdownSection({ + children, + title, + className, +}: DropdownSectionProps) { + const { container, menu, header } = styles(); + // TODO: fix accessibility of header and section + return ( +
+ {title &&
{title}
} +
{children}
+
+ ); +} diff --git a/libs/ui/dropdown/src/lib/DropdownSeparator.tsx b/libs/ui/dropdown/src/lib/DropdownSeparator.tsx new file mode 100644 index 00000000..1263138b --- /dev/null +++ b/libs/ui/dropdown/src/lib/DropdownSeparator.tsx @@ -0,0 +1,5 @@ +import { Separator } from 'react-aria-components'; + +export function DropdownSeparator() { + return ; +} diff --git a/libs/ui/dropdown/src/lib/DropdownTrigger.tsx b/libs/ui/dropdown/src/lib/DropdownTrigger.tsx new file mode 100644 index 00000000..8289e006 --- /dev/null +++ b/libs/ui/dropdown/src/lib/DropdownTrigger.tsx @@ -0,0 +1,10 @@ +import type { PropsWithChildren } from 'react'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface DropdownTriggerProps {} + +export function DropdownTrigger({ + children, +}: PropsWithChildren) { + return children; +} diff --git a/libs/ui/dropdown/tsconfig.json b/libs/ui/dropdown/tsconfig.json new file mode 100644 index 00000000..4daaf45c --- /dev/null +++ b/libs/ui/dropdown/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json" +} diff --git a/libs/ui/dropdown/tsconfig.lib.json b/libs/ui/dropdown/tsconfig.lib.json new file mode 100644 index 00000000..8baf71b1 --- /dev/null +++ b/libs/ui/dropdown/tsconfig.lib.json @@ -0,0 +1,23 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": [ + "node", + + "@nx/react/typings/cssmodule.d.ts", + "@nx/react/typings/image.d.ts" + ] + }, + "exclude": [ + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx" + ], + "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] +} diff --git a/libs/ui/dropdown/tsconfig.spec.json b/libs/ui/dropdown/tsconfig.spec.json new file mode 100644 index 00000000..05a0e183 --- /dev/null +++ b/libs/ui/dropdown/tsconfig.spec.json @@ -0,0 +1,26 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ] + }, + "include": [ + "vite.config.ts", + "vitest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] +} diff --git a/libs/ui/dropdown/vite.config.ts b/libs/ui/dropdown/vite.config.ts new file mode 100644 index 00000000..1362f7f6 --- /dev/null +++ b/libs/ui/dropdown/vite.config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + +export default defineConfig({ + root: __dirname, + cacheDir: '../../../node_modules/.vite/libs/ui/menu', + + plugins: [react(), nxViteTsPaths()], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + test: { + globals: true, + cache: { dir: '../../../node_modules/.vitest' }, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../../coverage/libs/ui/menu', + provider: 'v8', + }, + }, +}); diff --git a/libs/ui/error/.babelrc b/libs/ui/error/.babelrc new file mode 100644 index 00000000..1ea870ea --- /dev/null +++ b/libs/ui/error/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/ui/error/.eslintrc.json b/libs/ui/error/.eslintrc.json new file mode 100644 index 00000000..75b85077 --- /dev/null +++ b/libs/ui/error/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nx/react", "../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/ui/error/README.md b/libs/ui/error/README.md new file mode 100644 index 00000000..301a1eaf --- /dev/null +++ b/libs/ui/error/README.md @@ -0,0 +1,7 @@ +# error + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test error` to execute the unit tests via [Vitest](https://vitest.dev/). diff --git a/libs/ui/error/project.json b/libs/ui/error/project.json new file mode 100644 index 00000000..6c50d40c --- /dev/null +++ b/libs/ui/error/project.json @@ -0,0 +1,9 @@ +{ + "name": "error", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/ui/error/src", + "projectType": "library", + "tags": [], + "// targets": "to see all targets run: nx show project error --web", + "targets": {} +} diff --git a/libs/ui/error/src/index.ts b/libs/ui/error/src/index.ts new file mode 100644 index 00000000..03300822 --- /dev/null +++ b/libs/ui/error/src/index.ts @@ -0,0 +1,3 @@ +export * from './lib/ErrorBanner'; +export * from './lib/InlineError'; +export * from './lib/CrashError'; diff --git a/libs/ui/error/src/lib/CrashError.tsx b/libs/ui/error/src/lib/CrashError.tsx new file mode 100644 index 00000000..9f63ab3f --- /dev/null +++ b/libs/ui/error/src/lib/CrashError.tsx @@ -0,0 +1,31 @@ +import { useRouteError } from '@remix-run/react'; +import { UnauthorizedError } from '@restate/util/errors'; +import { Link } from '@restate/ui/link'; + +export function CrashError() { + const error = useRouteError(); + console.error(error); + + if (error instanceof UnauthorizedError) { + return null; + } + + return ( +
+

+ Oops something went wrong! +

+

+ Sorry, we couldn’t load what you’re looking for. +

+
+ + Go back home + + + Contact support + +
+
+ ); +} diff --git a/libs/ui/error/src/lib/ErrorBanner.tsx b/libs/ui/error/src/lib/ErrorBanner.tsx new file mode 100644 index 00000000..da60110b --- /dev/null +++ b/libs/ui/error/src/lib/ErrorBanner.tsx @@ -0,0 +1,98 @@ +import { Icon, IconName } from '@restate/ui/icons'; +import { PropsWithChildren } from 'react'; +import { tv } from 'tailwind-variants'; + +export interface ErrorProps { + errors?: + | Error[] + | string[] + | { + message: string; + restate_code?: string | null; + }[]; + className?: string; +} + +const styles = tv({ + base: 'rounded-xl bg-red-100 p-3', +}); + +function SingleError({ + error, + children, + className, +}: PropsWithChildren<{ + error?: + | Error + | string + | { + message: string; + restate_code?: string | null; + }; + className?: string; +}>) { + if (!error) { + return null; + } + + return ( +
+
+
+ +
+ + {typeof error === 'string' ? error : error.message} + + {children &&
{children}
} +
+
+ ); +} + +export function ErrorBanner({ + errors = [], + children, + className, +}: PropsWithChildren) { + if (errors.length === 0) { + return null; + } + if (errors.length === 1) { + const [error] = errors; + return ( + + ); + } + + return ( +
+
+
+ +
+
+

+ There were {errors.length} errors: +

+ +
    + {errors.map((error) => ( +
  • + {typeof error === 'string' ? error : error.message} +
  • + ))} +
+
+ {children} +
+
+
+ ); +} diff --git a/libs/ui/error/src/lib/InlineError.tsx b/libs/ui/error/src/lib/InlineError.tsx new file mode 100644 index 00000000..dda9cca1 --- /dev/null +++ b/libs/ui/error/src/lib/InlineError.tsx @@ -0,0 +1,25 @@ +import { Icon, IconName } from '@restate/ui/icons'; +import { PropsWithChildren } from 'react'; +import { tv } from 'tailwind-variants'; + +export interface InlineErrorProps { + className?: string; +} + +const styles = tv({ + base: 'inline-flex gap-1 items-center text-start text-red-600', +}); +export function InlineError({ + children, + className, +}: PropsWithChildren) { + return ( + + + {children} + + ); +} diff --git a/libs/ui/error/tsconfig.json b/libs/ui/error/tsconfig.json new file mode 100644 index 00000000..4daaf45c --- /dev/null +++ b/libs/ui/error/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json" +} diff --git a/libs/ui/error/tsconfig.lib.json b/libs/ui/error/tsconfig.lib.json new file mode 100644 index 00000000..fc5856c4 --- /dev/null +++ b/libs/ui/error/tsconfig.lib.json @@ -0,0 +1,24 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": [ + "node", + + "@nx/react/typings/cssmodule.d.ts", + "@nx/react/typings/image.d.ts", + "../../../@types/global-env.d.ts" + ] + }, + "exclude": [ + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx" + ], + "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] +} diff --git a/libs/ui/error/tsconfig.spec.json b/libs/ui/error/tsconfig.spec.json new file mode 100644 index 00000000..05a0e183 --- /dev/null +++ b/libs/ui/error/tsconfig.spec.json @@ -0,0 +1,26 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ] + }, + "include": [ + "vite.config.ts", + "vitest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] +} diff --git a/libs/ui/error/vite.config.ts b/libs/ui/error/vite.config.ts new file mode 100644 index 00000000..842df1e6 --- /dev/null +++ b/libs/ui/error/vite.config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + +export default defineConfig({ + root: __dirname, + cacheDir: '../../../node_modules/.vite/libs/ui/error', + + plugins: [react(), nxViteTsPaths()], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + test: { + globals: true, + cache: { dir: '../../../node_modules/.vitest' }, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../../coverage/libs/ui/error', + provider: 'v8', + }, + }, +}); diff --git a/libs/ui/focus/.babelrc b/libs/ui/focus/.babelrc new file mode 100644 index 00000000..1ea870ea --- /dev/null +++ b/libs/ui/focus/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/ui/focus/.eslintrc.json b/libs/ui/focus/.eslintrc.json new file mode 100644 index 00000000..75b85077 --- /dev/null +++ b/libs/ui/focus/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nx/react", "../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/ui/focus/README.md b/libs/ui/focus/README.md new file mode 100644 index 00000000..ddac937f --- /dev/null +++ b/libs/ui/focus/README.md @@ -0,0 +1,7 @@ +# ui-focus + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test ui-focus` to execute the unit tests via [Vitest](https://vitest.dev/). diff --git a/libs/ui/focus/project.json b/libs/ui/focus/project.json new file mode 100644 index 00000000..352a8619 --- /dev/null +++ b/libs/ui/focus/project.json @@ -0,0 +1,9 @@ +{ + "name": "focus", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/ui/focus/src", + "projectType": "library", + "tags": [], + "// targets": "to see all targets run: nx show project ui-focus --web", + "targets": {} +} diff --git a/libs/ui/focus/src/index.ts b/libs/ui/focus/src/index.ts new file mode 100644 index 00000000..10c246a3 --- /dev/null +++ b/libs/ui/focus/src/index.ts @@ -0,0 +1 @@ +export * from './lib/focus'; diff --git a/libs/ui/focus/src/lib/focus.tsx b/libs/ui/focus/src/lib/focus.tsx new file mode 100644 index 00000000..b5872e95 --- /dev/null +++ b/libs/ui/focus/src/lib/focus.tsx @@ -0,0 +1,11 @@ +import { tv } from 'tailwind-variants'; + +export const focusRing = tv({ + base: 'outline outline-blue-600 outline-offset-2', + variants: { + isFocusVisible: { + false: 'outline-0', + true: 'outline-2', + }, + }, +}); diff --git a/libs/ui/focus/tsconfig.json b/libs/ui/focus/tsconfig.json new file mode 100644 index 00000000..4daaf45c --- /dev/null +++ b/libs/ui/focus/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json" +} diff --git a/libs/ui/focus/tsconfig.lib.json b/libs/ui/focus/tsconfig.lib.json new file mode 100644 index 00000000..8baf71b1 --- /dev/null +++ b/libs/ui/focus/tsconfig.lib.json @@ -0,0 +1,23 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": [ + "node", + + "@nx/react/typings/cssmodule.d.ts", + "@nx/react/typings/image.d.ts" + ] + }, + "exclude": [ + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx" + ], + "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] +} diff --git a/libs/ui/focus/tsconfig.spec.json b/libs/ui/focus/tsconfig.spec.json new file mode 100644 index 00000000..05a0e183 --- /dev/null +++ b/libs/ui/focus/tsconfig.spec.json @@ -0,0 +1,26 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ] + }, + "include": [ + "vite.config.ts", + "vitest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] +} diff --git a/libs/ui/focus/vite.config.ts b/libs/ui/focus/vite.config.ts new file mode 100644 index 00000000..6799f354 --- /dev/null +++ b/libs/ui/focus/vite.config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + +export default defineConfig({ + root: __dirname, + cacheDir: '../../../node_modules/.vite/libs/ui/focus', + + plugins: [react(), nxViteTsPaths()], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + test: { + globals: true, + cache: { dir: '../../../node_modules/.vitest' }, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../../coverage/libs/ui/focus', + provider: 'v8', + }, + }, +}); diff --git a/libs/ui/form-field/.babelrc b/libs/ui/form-field/.babelrc new file mode 100644 index 00000000..1ea870ea --- /dev/null +++ b/libs/ui/form-field/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/ui/form-field/.eslintrc.json b/libs/ui/form-field/.eslintrc.json new file mode 100644 index 00000000..75b85077 --- /dev/null +++ b/libs/ui/form-field/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nx/react", "../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/ui/form-field/README.md b/libs/ui/form-field/README.md new file mode 100644 index 00000000..be801e2e --- /dev/null +++ b/libs/ui/form-field/README.md @@ -0,0 +1,7 @@ +# ui-form-field + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test ui-form-field` to execute the unit tests via [Vitest](https://vitest.dev/). diff --git a/libs/ui/form-field/project.json b/libs/ui/form-field/project.json new file mode 100644 index 00000000..23c8866b --- /dev/null +++ b/libs/ui/form-field/project.json @@ -0,0 +1,9 @@ +{ + "name": "form-field", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/ui/form-field/src", + "projectType": "library", + "tags": [], + "// targets": "to see all targets run: nx show project ui-form-field --web", + "targets": {} +} diff --git a/libs/ui/form-field/src/index.ts b/libs/ui/form-field/src/index.ts new file mode 100644 index 00000000..b4b0fe43 --- /dev/null +++ b/libs/ui/form-field/src/index.ts @@ -0,0 +1,7 @@ +export * from './lib/FormFieldError'; +export * from './lib/FormFieldGroup'; +export * from './lib/FormFieldLabel'; +export * from './lib/FormFieldInput'; +export * from './lib/FormFieldTextarea'; +export * from './lib/FormFieldCheckbox'; +export * from './lib/FormFieldSelect'; diff --git a/libs/ui/form-field/src/lib/FormFieldCheckbox.tsx b/libs/ui/form-field/src/lib/FormFieldCheckbox.tsx new file mode 100644 index 00000000..3d7aa2ad --- /dev/null +++ b/libs/ui/form-field/src/lib/FormFieldCheckbox.tsx @@ -0,0 +1,72 @@ +import { + TextFieldProps as AriaTextFieldProps, + Input, + TextField, + Label, +} from 'react-aria-components'; +import { tv } from 'tailwind-variants'; +import { FormFieldError } from './FormFieldError'; +import { ComponentProps, PropsWithChildren, forwardRef } from 'react'; + +interface FormFieldCheckboxProps + extends Pick< + AriaTextFieldProps, + 'name' | 'value' | 'defaultValue' | 'autoFocus' + > { + className?: string; + required?: boolean; + disabled?: boolean; + errorMessage?: ComponentProps['children']; + slot?: string; + checked?: boolean; + direction?: 'left' | 'right'; +} + +const styles = tv({ + slots: { + label: 'row-start-1 text-base', + container: 'grid gap-x-2 items-center', + input: + 'disabled:text-gray-100 hover:disabled:text-gray-100 focus:disabled:text-gray-100 disabled:bg-gray-100 disabled:border-gray-100 disabled:shadow-none invalid:bg-red-100 invalid:border-red-600 text-blue-600 checked:focus:text-blue-800 bg-gray-100 row-start-1 min-w-0 rounded-md w-5 h-5 border-gray-200 focus:bg-gray-300 hover:bg-gray-300 shadow-[inset_0_0.5px_0.5px_0px_rgba(0,0,0,0.08)]', + }, + variants: { + direction: { + left: { + container: 'grid-cols-[1.25rem_1fr]', + input: 'col-start-1', + label: 'col-start-2', + }, + right: { + container: 'grid-cols-[1fr_1.25rem]', + input: 'col-start-2', + label: 'col-start-1', + }, + }, + }, +}); +export const FormFieldCheckbox = forwardRef< + HTMLInputElement, + PropsWithChildren +>( + ( + { className, errorMessage, children, direction = 'left', ...props }, + ref + ) => { + const { input, container, label } = styles({ direction }); + return ( + + + + + + ); + } +); diff --git a/libs/ui/form-field/src/lib/FormFieldError.tsx b/libs/ui/form-field/src/lib/FormFieldError.tsx new file mode 100644 index 00000000..430834d8 --- /dev/null +++ b/libs/ui/form-field/src/lib/FormFieldError.tsx @@ -0,0 +1,16 @@ +import { + FieldError as AriaFieldError, + FieldErrorProps, +} from 'react-aria-components'; +import { tv } from 'tailwind-variants'; + +interface FormFieldErrorProps extends Pick { + className?: string; +} + +const styles = tv({ + base: 'text-xs px-1 pt-0.5 text-red-600 forced-colors:text-[Mark]', +}); +export function FormFieldError({ className, ...props }: FormFieldErrorProps) { + return ; +} diff --git a/libs/ui/form-field/src/lib/FormFieldGroup.tsx b/libs/ui/form-field/src/lib/FormFieldGroup.tsx new file mode 100644 index 00000000..7ca407a7 --- /dev/null +++ b/libs/ui/form-field/src/lib/FormFieldGroup.tsx @@ -0,0 +1,43 @@ +import { focusRing } from '@restate/ui/focus'; +import { tv } from 'tailwind-variants'; +import { Group as AriaGroup } from 'react-aria-components'; +import { PropsWithChildren } from 'react'; + +interface FormFieldGroupProps { + className?: string; +} + +const fieldBorderStyles = tv({ + variants: { + isFocusWithin: { + false: 'border-gray-300 dark:border-zinc-500', + true: 'border-gray-600 dark:border-zinc-300 rounded-[0.625rem] outline-offset-8', + }, + isInvalid: { + true: 'border-red-600 dark:border-red-600 forced-colors:border-[Mark]', + }, + isDisabled: { + true: 'border-gray-200 dark:border-zinc-700 forced-colors:border-[GrayText]', + }, + }, +}); + +const fieldGroupStyles = tv({ + extend: focusRing, + base: 'group flex items-start flex-col', + variants: fieldBorderStyles.variants, +}); + +export function FormFieldGroup({ + className, + ...props +}: PropsWithChildren) { + return ( + + fieldGroupStyles({ ...renderProps, className }) + } + /> + ); +} diff --git a/libs/ui/form-field/src/lib/FormFieldInput.tsx b/libs/ui/form-field/src/lib/FormFieldInput.tsx new file mode 100644 index 00000000..9e0e2b33 --- /dev/null +++ b/libs/ui/form-field/src/lib/FormFieldInput.tsx @@ -0,0 +1,71 @@ +import { + TextFieldProps as AriaTextFieldProps, + Input as AriaInput, + TextField, + Label, +} from 'react-aria-components'; +import { tv } from 'tailwind-variants'; +import { FormFieldError } from './FormFieldError'; +import { ComponentProps, ReactNode } from 'react'; +import { FormFieldLabel } from './FormFieldLabel'; + +const inputStyles = tv({ + base: 'invalid:border-red-600 invalid:bg-red-100/70 focus:outline focus:border-gray-200 disabled:text-gray-500/80 disabled:placeholder:text-gray-300 disabled:border-gray-100 disabled:shadow-none [&[readonly]]:text-gray-500/80 [&[readonly]]:bg-gray-100 read-only:shadow-none focus:shadow-none focus:outline-blue-600 focus:[box-shadow:inset_0_1px_0px_0px_rgba(0,0,0,0.03)] shadow-[inset_0_1px_0px_0px_rgba(0,0,0,0.03)] mt-0 bg-gray-100 rounded-lg border border-gray-200 py-1.5 placeholder:text-gray-500/70 px-2 w-full min-w-0 text-sm text-gray-900', +}); +const containerStyles = tv({ + base: '', +}); + +interface InputProps + extends Pick< + AriaTextFieldProps, + | 'name' + | 'value' + | 'defaultValue' + | 'autoFocus' + | 'autoComplete' + | 'validate' + | 'pattern' + | 'maxLength' + | 'type' + | 'onChange' + > { + className?: string; + required?: boolean; + disabled?: boolean; + readonly?: boolean; + placeholder?: string; + label?: ReactNode; + errorMessage?: ComponentProps['children']; +} +export function FormFieldInput({ + className, + required, + disabled, + autoComplete = 'off', + placeholder, + errorMessage, + label, + readonly, + ...props +}: InputProps) { + return ( + + {!label && } + {label && {label}} + + + + ); +} diff --git a/libs/ui/form-field/src/lib/FormFieldLabel.tsx b/libs/ui/form-field/src/lib/FormFieldLabel.tsx new file mode 100644 index 00000000..7205033e --- /dev/null +++ b/libs/ui/form-field/src/lib/FormFieldLabel.tsx @@ -0,0 +1,14 @@ +import { PropsWithChildren } from 'react'; +import { Label as AriaLabel } from 'react-aria-components'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface FormFieldLabelProps {} + +export function FormFieldLabel(props: PropsWithChildren) { + return ( + + ); +} diff --git a/libs/ui/form-field/src/lib/FormFieldSelect.tsx b/libs/ui/form-field/src/lib/FormFieldSelect.tsx new file mode 100644 index 00000000..59af4a22 --- /dev/null +++ b/libs/ui/form-field/src/lib/FormFieldSelect.tsx @@ -0,0 +1,87 @@ +import { + SelectProps as AriaSelectProps, + Label, + Select, + SelectValue, +} from 'react-aria-components'; +import { tv } from 'tailwind-variants'; +import { FormFieldError } from './FormFieldError'; +import { ComponentProps, PropsWithChildren, ReactNode } from 'react'; +import { Button } from '@restate/ui/button'; +import { PopoverOverlay } from '@restate/ui/popover'; +import { ListBox, ListBoxItem } from '@restate/ui/listbox'; +import { FormFieldLabel } from './FormFieldLabel'; +import { Icon, IconName } from '@restate/ui/icons'; + +const containerStyles = tv({ + base: '', +}); + +interface SelectProps + extends Pick< + AriaSelectProps, + 'name' | 'autoFocus' | 'autoComplete' | 'validate' + > { + className?: string; + required?: boolean; + disabled?: boolean; + placeholder?: string; + errorMessage?: ComponentProps['children']; + label?: ReactNode; +} +export function FormFieldSelect({ + className, + required, + disabled, + autoComplete = 'off', + placeholder, + errorMessage, + children, + label, + autoFocus, + ...props +}: PropsWithChildren) { + return ( + + ); +} + +export function Option({ children }: { children: string }) { + return ( + + {children} + + ); +} diff --git a/libs/ui/form-field/src/lib/FormFieldTextarea.tsx b/libs/ui/form-field/src/lib/FormFieldTextarea.tsx new file mode 100644 index 00000000..fee574f9 --- /dev/null +++ b/libs/ui/form-field/src/lib/FormFieldTextarea.tsx @@ -0,0 +1,61 @@ +import { + TextAreaProps as AriaTextAreaProps, + TextArea as AriaTextArea, + TextField, + Label, +} from 'react-aria-components'; +import { tv } from 'tailwind-variants'; +import { FormFieldError } from './FormFieldError'; +import { ComponentProps, ReactNode } from 'react'; +import { FormFieldLabel } from './FormFieldLabel'; + +const inputStyles = tv({ + base: 'flex-1 invalid:border-red-600 invalid:bg-red-100/70 focus:outline focus:border-gray-200 disabled:text-gray-500/80 disabled:placeholder:text-gray-300 disabled:bg-gray-100 disabled:border-gray-100 disabled:shadow-none focus:shadow-none focus:outline-blue-600 focus:[box-shadow:inset_0_1px_0px_0px_rgba(0,0,0,0.03)] shadow-[inset_0_1px_0px_0px_rgba(0,0,0,0.03)] mt-0 bg-gray-100 rounded-lg border border-gray-200 py-1.5 placeholder:text-gray-500/70 px-2 w-full min-w-0 text-sm text-gray-900', +}); +const containerStyles = tv({ + base: 'flex flex-col', +}); + +interface FormFieldTextarea + extends Pick< + AriaTextAreaProps, + 'name' | 'autoFocus' | 'autoComplete' | 'maxLength' | 'rows' + > { + className?: string; + required?: boolean; + disabled?: boolean; + placeholder?: string; + label?: ReactNode; + errorMessage?: ComponentProps['children']; + value?: string; + defaultValue?: string; +} +export function FormFieldTextarea({ + className, + required, + disabled, + autoComplete = 'off', + placeholder, + errorMessage, + label, + ...props +}: FormFieldTextarea) { + return ( + + {!label && } + {label && {label}} + + + + ); +} diff --git a/libs/ui/form-field/tsconfig.json b/libs/ui/form-field/tsconfig.json new file mode 100644 index 00000000..4daaf45c --- /dev/null +++ b/libs/ui/form-field/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json" +} diff --git a/libs/ui/form-field/tsconfig.lib.json b/libs/ui/form-field/tsconfig.lib.json new file mode 100644 index 00000000..8baf71b1 --- /dev/null +++ b/libs/ui/form-field/tsconfig.lib.json @@ -0,0 +1,23 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": [ + "node", + + "@nx/react/typings/cssmodule.d.ts", + "@nx/react/typings/image.d.ts" + ] + }, + "exclude": [ + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx" + ], + "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] +} diff --git a/libs/ui/form-field/tsconfig.spec.json b/libs/ui/form-field/tsconfig.spec.json new file mode 100644 index 00000000..05a0e183 --- /dev/null +++ b/libs/ui/form-field/tsconfig.spec.json @@ -0,0 +1,26 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ] + }, + "include": [ + "vite.config.ts", + "vitest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] +} diff --git a/libs/ui/form-field/vite.config.ts b/libs/ui/form-field/vite.config.ts new file mode 100644 index 00000000..8fb8559e --- /dev/null +++ b/libs/ui/form-field/vite.config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + +export default defineConfig({ + root: __dirname, + cacheDir: '../../../node_modules/.vite/libs/ui/form-field', + + plugins: [react(), nxViteTsPaths()], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + test: { + globals: true, + cache: { dir: '../../../node_modules/.vitest' }, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../../coverage/libs/ui/form-field', + provider: 'v8', + }, + }, +}); diff --git a/libs/ui/icons/.babelrc b/libs/ui/icons/.babelrc new file mode 100644 index 00000000..1ea870ea --- /dev/null +++ b/libs/ui/icons/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/ui/icons/.eslintrc.json b/libs/ui/icons/.eslintrc.json new file mode 100644 index 00000000..75b85077 --- /dev/null +++ b/libs/ui/icons/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nx/react", "../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/ui/icons/README.md b/libs/ui/icons/README.md new file mode 100644 index 00000000..5b625c02 --- /dev/null +++ b/libs/ui/icons/README.md @@ -0,0 +1,7 @@ +# ui-icons + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test ui-icons` to execute the unit tests via [Vitest](https://vitest.dev/). diff --git a/libs/ui/icons/project.json b/libs/ui/icons/project.json new file mode 100644 index 00000000..1e3d0909 --- /dev/null +++ b/libs/ui/icons/project.json @@ -0,0 +1,9 @@ +{ + "name": "icons", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/ui/icons/src", + "projectType": "library", + "tags": [], + "// targets": "to see all targets run: nx show project ui-icons --web", + "targets": {} +} diff --git a/libs/ui/icons/src/index.ts b/libs/ui/icons/src/index.ts new file mode 100644 index 00000000..de76a4b2 --- /dev/null +++ b/libs/ui/icons/src/index.ts @@ -0,0 +1 @@ +export * from './lib/Icons'; diff --git a/libs/ui/icons/src/lib/Icons.tsx b/libs/ui/icons/src/lib/Icons.tsx new file mode 100644 index 00000000..1e19ab85 --- /dev/null +++ b/libs/ui/icons/src/lib/Icons.tsx @@ -0,0 +1,135 @@ +import { + Check, + ChevronDown, + ChevronRight, + ChevronsUpDown, + Plus, + LogOut, + Squircle, + Trash, + Circle, + CircleDashed, + CircleDotDashed, + TriangleAlert, + Minus, + Copy, + RotateCw, + SquareCheckBig, + Terminal, + Lock, + FileKey, + Globe, + FileClock, + ExternalLink, + Wallet, + X, + Box, + SquareFunction, +} from 'lucide-react'; +import type { LucideIcon } from 'lucide-react'; +import { tv } from 'tailwind-variants'; +import { RestateEnvironment } from './custom-icons/RestateEnvironment'; +import { Restate } from './custom-icons/Restate'; +import { CircleX } from './custom-icons/CircleX'; +import { Lambda } from './custom-icons/Lambda'; +import { Docs } from './custom-icons/Docs'; +import { Github } from './custom-icons/Github'; +import { Discord } from './custom-icons/Discord'; +import { SupportTicket } from './custom-icons/SupportTicket'; +import { Help } from './custom-icons/Help'; + +export const enum IconName { + ChevronDown = 'ChevronDown', + ChevronRight = 'ChevronRight', + Check = 'Check', + RestateEnvironment = 'RestateEnvironment', + Restate = 'Restate', + ChevronsUpDown = 'ChevronsUpDown', + Plus = 'Plus', + LogOut = 'LogOut', + Squircle = 'Squircle', + Trash = 'Trash', + Circle = 'Circle', + CircleDashed = 'CircleDashed', + TriangleAlert = 'TriangleAlert', + CircleDotDashed = 'CircleDotDashed', + CircleX = 'CircleX', + Minus = 'Minus', + Copy = 'Copy', + Retry = 'Retry', + SquareCheckBig = 'SquareCheckBig', + Http = 'Http', + Security = 'Security', + ApiKey = 'ApiKey', + Cli = 'Cli', + Log = 'Log', + ExternalLink = 'ExternalLink', + Wallet = 'Wallet', + X = 'X', + Docs = 'Docs', + Discord = 'Discord', + Github = 'Github', + SupportTicket = 'SupportTicket', + Help = 'Help', + Lambda = 'Lambda', + Box = 'Box', + Function = 'SquareFunction', +} +export interface IconsProps { + name: IconName; + ['aria-hidden']?: boolean; + className?: string; +} + +const ICONS: Record = { + [IconName.ChevronDown]: ChevronDown, + [IconName.Check]: Check, + [IconName.ChevronRight]: ChevronRight, + [IconName.ChevronsUpDown]: ChevronsUpDown, + [IconName.Plus]: Plus, + [IconName.LogOut]: LogOut, + [IconName.RestateEnvironment]: RestateEnvironment, + [IconName.Squircle]: Squircle, + [IconName.Trash]: Trash, + [IconName.Circle]: Circle, + [IconName.CircleDashed]: CircleDashed, + [IconName.TriangleAlert]: TriangleAlert, + [IconName.CircleDotDashed]: CircleDotDashed, + [IconName.CircleX]: CircleX, + [IconName.Minus]: Minus, + [IconName.Copy]: Copy, + [IconName.SquareCheckBig]: SquareCheckBig, + [IconName.Retry]: RotateCw, + [IconName.Security]: Lock, + [IconName.Cli]: Terminal, + [IconName.ApiKey]: FileKey, + [IconName.Http]: Globe, + [IconName.Log]: FileClock, + [IconName.ExternalLink]: ExternalLink, + [IconName.Wallet]: Wallet, + [IconName.X]: X, + [IconName.Restate]: Restate, + [IconName.Docs]: Docs, + [IconName.Github]: Github, + [IconName.Discord]: Discord, + [IconName.SupportTicket]: SupportTicket, + [IconName.Help]: Help, + [IconName.Lambda]: Lambda, + [IconName.Box]: Box, + [IconName.Function]: SquareFunction, +}; + +const styles = tv({ + base: 'w-[1.5em] h-[1.5em] text-current', +}); + +export function Icon({ name, className, ...props }: IconsProps) { + const IconComponent = ICONS[name]; + return ( +